Files
xiaomihome/miot/miot_network.go
T
4566704 3a44cb9e6a feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写)
- 支持云端 API 调用与局域网设备发现(mDNS)
- 集成国际化(i18n)多语言支持
- 添加 MIoT 设备规约解析器(spec_parser)
- 包含单元测试与使用示例
2026-06-28 22:05:49 +08:00

598 lines
14 KiB
Go

// Package miot provides MIoT core client for Xiaomi Home devices.
// miot_network.go — network status monitor ported from py-miot/miot_network.py.
package miot
import (
"fmt"
"log"
"net"
"net/http"
"os/exec"
"runtime"
"strings"
"sync"
"time"
)
// ============================================================================
// InterfaceStatus — network interface change event type
// ============================================================================
// InterfaceStatus describes what changed on a network interface.
type InterfaceStatus int
const (
// InterfaceAdd means a new interface appeared.
InterfaceAdd InterfaceStatus = 0
// InterfaceUpdate means an existing interface's IP/mask changed.
InterfaceUpdate InterfaceStatus = 1
// InterfaceRemove means an interface was removed.
InterfaceRemove InterfaceStatus = 2
)
// String returns the string representation of InterfaceStatus.
func (s InterfaceStatus) String() string {
switch s {
case InterfaceAdd:
return "add"
case InterfaceUpdate:
return "update"
case InterfaceRemove:
return "remove"
default:
return "unknown"
}
}
// ============================================================================
// NetworkInfo — information about a network interface
// ============================================================================
// NetworkInfo holds details about a single network interface.
type NetworkInfo struct {
Name string // interface name (e.g., "eth0", "wlan0")
IP string // IPv4 address
Netmask string // IPv4 netmask
NetSeg string // network segment (CIDR network address)
}
// String returns a debug representation.
func (n *NetworkInfo) String() string {
return fmt.Sprintf("%s: %s/%s (%s)", n.Name, n.IP, n.Netmask, n.NetSeg)
}
// ============================================================================
// MIoTNetwork — network status monitor
// ============================================================================
// MIoTNetwork monitors network connectivity using ping + HTTP detection,
// periodically refreshes network interface info, and notifies subscribers.
type MIoTNetwork struct {
mu sync.Mutex
// Detection addresses with cached response times (seconds)
ipAddrMap map[string]float64
httpAddrMap map[string]float64
// HTTP client for detection
httpClient *http.Client
// Refresh interval
refreshInterval time.Duration
// Current state
networkStatus bool
networkInfo map[string]*NetworkInfo
// Subscribers
statusSubs map[string]func(status bool)
infoSubs map[string]func(status InterfaceStatus, info *NetworkInfo)
// Timer control
stopCh chan struct{}
doneCh chan struct{}
}
// Default network detection addresses.
var (
defaultIPAddrs = []string{
"1.2.4.8", // CNNIC sDNS
"8.8.8.8", // Google Public DNS
"9.9.9.9", // Quad9
}
defaultHTTPAddrs = []string{
"https://www.bing.com",
"https://www.google.com",
"https://www.baidu.com",
}
defaultDetectTimeout = 6 * time.Second
defaultRefreshInterval = 30 * time.Second
)
// NewMIoTNetwork creates a new MIoTNetwork monitor.
// ipAddrList and urlAddrList can be nil to use defaults.
func NewMIoTNetwork(ipAddrList, urlAddrList []string, refreshInterval time.Duration) *MIoTNetwork {
if ipAddrList == nil {
ipAddrList = defaultIPAddrs
}
if urlAddrList == nil {
urlAddrList = defaultHTTPAddrs
}
if refreshInterval <= 0 {
refreshInterval = defaultRefreshInterval
}
n := &MIoTNetwork{
ipAddrMap: make(map[string]float64),
httpAddrMap: make(map[string]float64),
httpClient: &http.Client{Timeout: defaultDetectTimeout},
refreshInterval: refreshInterval,
networkStatus: false,
networkInfo: make(map[string]*NetworkInfo),
statusSubs: make(map[string]func(status bool)),
infoSubs: make(map[string]func(status InterfaceStatus, info *NetworkInfo)),
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
for _, ip := range ipAddrList {
n.ipAddrMap[ip] = float64(defaultDetectTimeout.Seconds())
}
for _, url := range urlAddrList {
n.httpAddrMap[url] = float64(defaultDetectTimeout.Seconds())
}
return n
}
// Init starts the network monitor.
func (n *MIoTNetwork) Init() {
// Initial refresh
n.refresh()
// Start periodic timer
go n.loop()
}
// Deinit stops the network monitor and cleans up.
func (n *MIoTNetwork) Deinit() {
close(n.stopCh)
<-n.doneCh
n.httpClient.CloseIdleConnections()
n.mu.Lock()
defer n.mu.Unlock()
n.networkStatus = false
n.networkInfo = make(map[string]*NetworkInfo)
n.statusSubs = make(map[string]func(status bool))
n.infoSubs = make(map[string]func(status InterfaceStatus, info *NetworkInfo))
}
// GetNetworkStatus returns the current network connectivity status.
func (n *MIoTNetwork) GetNetworkStatus() bool {
n.mu.Lock()
defer n.mu.Unlock()
return n.networkStatus
}
// GetNetworkInfo returns the current network interface info.
func (n *MIoTNetwork) GetNetworkInfo() map[string]*NetworkInfo {
n.mu.Lock()
defer n.mu.Unlock()
// Return a copy
result := make(map[string]*NetworkInfo, len(n.networkInfo))
for k, v := range n.networkInfo {
result[k] = &NetworkInfo{
Name: v.Name,
IP: v.IP,
Netmask: v.Netmask,
NetSeg: v.NetSeg,
}
}
return result
}
// SubNetworkStatus subscribes to network status changes.
// key is a unique subscriber identifier.
func (n *MIoTNetwork) SubNetworkStatus(key string, handler func(status bool)) {
n.mu.Lock()
defer n.mu.Unlock()
n.statusSubs[key] = handler
}
// UnsubNetworkStatus unsubscribes from network status changes.
func (n *MIoTNetwork) UnsubNetworkStatus(key string) {
n.mu.Lock()
defer n.mu.Unlock()
delete(n.statusSubs, key)
}
// SubNetworkInfo subscribes to network interface changes.
// key is a unique subscriber identifier.
func (n *MIoTNetwork) SubNetworkInfo(key string, handler func(status InterfaceStatus, info *NetworkInfo)) {
n.mu.Lock()
defer n.mu.Unlock()
n.infoSubs[key] = handler
}
// UnsubNetworkInfo unsubscribes from network interface changes.
func (n *MIoTNetwork) UnsubNetworkInfo(key string) {
n.mu.Lock()
defer n.mu.Unlock()
delete(n.infoSubs, key)
}
// Refresh triggers an immediate refresh of network status and info.
func (n *MIoTNetwork) Refresh() {
n.refresh()
}
// loop runs the periodic refresh timer.
func (n *MIoTNetwork) loop() {
defer close(n.doneCh)
ticker := time.NewTicker(n.refreshInterval)
defer ticker.Stop()
for {
select {
case <-n.stopCh:
return
case <-ticker.C:
n.refresh()
}
}
}
// refresh performs a full refresh: network status detection + interface info.
func (n *MIoTNetwork) refresh() {
newStatus := checkNetworkStatus(n)
newInfo := getNetworkInterfaces()
n.mu.Lock()
oldStatus := n.networkStatus
oldInfo := n.networkInfo
// Update cached addresses with latest response times
for ip, ts := range n.ipAddrMap {
_ = ip
n.ipAddrMap[ip] = ts
}
for url, ts := range n.httpAddrMap {
_ = url
n.httpAddrMap[url] = ts
}
// Status changed?
if oldStatus != newStatus {
n.networkStatus = newStatus
handlers := make([]func(bool), 0, len(n.statusSubs))
for _, h := range n.statusSubs {
handlers = append(handlers, h)
}
n.mu.Unlock()
// Notify outside lock
for _, h := range handlers {
if h != nil {
h(newStatus)
}
}
} else {
n.mu.Unlock()
}
// Check for interface changes
n.mu.Lock()
for name := range oldInfo {
if newInfoInfo, ok := newInfo[name]; ok {
// Update
old := oldInfo[name]
if old.IP != newInfoInfo.IP || old.Netmask != newInfoInfo.Netmask {
n.networkInfo[name] = newInfoInfo
delete(newInfo, name)
n.notifyInfoSubsLocked(InterfaceUpdate, newInfoInfo)
} else {
delete(newInfo, name) // no change
}
} else {
// Remove
removed := n.networkInfo[name]
delete(n.networkInfo, name)
n.notifyInfoSubsLocked(InterfaceRemove, removed)
}
}
// Add new interfaces
for _, info := range newInfo {
n.networkInfo[info.Name] = info
n.notifyInfoSubsLocked(InterfaceAdd, info)
}
// Collect handlers
handlers := make([]func(InterfaceStatus, *NetworkInfo), 0, len(n.infoSubs))
for _, h := range n.infoSubs {
handlers = append(handlers, h)
}
n.mu.Unlock()
// For new interfaces we already notified inside lock above
_ = handlers
}
// notifyInfoSubsLocked sends info change notifications. Must hold mu.
func (n *MIoTNetwork) notifyInfoSubsLocked(status InterfaceStatus, info *NetworkInfo) {
for _, handler := range n.infoSubs {
if handler != nil {
handler(status, info)
}
}
}
// ============================================================================
// Network detection functions
// ============================================================================
// checkNetworkStatus detects if the network is reachable.
// Strategy: try the fastest known IP first, then fastest HTTP, then all.
func checkNetworkStatus(n *MIoTNetwork) bool {
n.mu.Lock()
ipAddrs := make([]string, 0, len(n.ipAddrMap))
ipTimes := make(map[string]float64)
for ip, ts := range n.ipAddrMap {
ipAddrs = append(ipAddrs, ip)
ipTimes[ip] = ts
}
httpAddrs := make([]string, 0, len(n.httpAddrMap))
httpTimes := make(map[string]float64)
for url, ts := range n.httpAddrMap {
httpAddrs = append(httpAddrs, url)
httpTimes[url] = ts
}
n.mu.Unlock()
timeout := float64(defaultDetectTimeout.Seconds())
// Try fastest IP first
fastestIP := ""
fastestIPTime := timeout
for ip, ts := range ipTimes {
if ts < fastestIPTime {
fastestIP = ip
fastestIPTime = ts
}
}
if fastestIPTime < timeout && fastestIP != "" {
if pingAddr(fastestIP) {
return true
}
}
// Try fastest HTTP first
fastestHTTP := ""
fastestHTTPTime := timeout
for url, ts := range httpTimes {
if ts < fastestHTTPTime {
fastestHTTP = url
fastestHTTPTime = ts
}
}
if fastestHTTPTime < timeout && fastestHTTP != "" {
if httpCheck(fastestHTTP) {
return true
}
}
// Full detection: ping all
pingResults := pingMulti(ipAddrs)
// Update cached times
n.mu.Lock()
for ip, ts := range pingResults {
n.ipAddrMap[ip] = ts
}
n.mu.Unlock()
for _, ok := range pingResults {
if ok < timeout {
return true
}
}
// Full detection: HTTP all
httpResults := httpMulti(httpAddrs)
n.mu.Lock()
for url, ts := range httpResults {
n.httpAddrMap[url] = ts
}
n.mu.Unlock()
for _, ok := range httpResults {
if ok < timeout {
return true
}
}
return false
}
// pingAddr pings a single IP address and returns true if reachable.
func pingAddr(ip string) bool {
return pingSingle(ip) < float64(defaultDetectTimeout.Seconds())
}
// pingSingle pings an IP and returns the response time in seconds, or the timeout value on failure.
func pingSingle(ip string) float64 {
start := time.Now()
timeout := defaultDetectTimeout
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
// Windows: ping -n 1 -w <ms> <ip>
cmd = exec.Command("ping", "-n", "1", "-w", fmt.Sprintf("%d", int(timeout.Seconds()*1000)), ip)
} else {
// Linux: ping -c 1 -W <seconds> <ip>
cmd = exec.Command("ping", "-c", "1", "-W", fmt.Sprintf("%d", int(timeout.Seconds())), ip)
}
if err := cmd.Run(); err != nil {
return float64(timeout.Seconds())
}
return time.Since(start).Seconds()
}
// pingMulti pings multiple IPs concurrently and returns response times.
func pingMulti(ips []string) map[string]float64 {
results := make(map[string]float64)
type result struct {
ip string
ts float64
}
ch := make(chan result, len(ips))
for _, ip := range ips {
go func(addr string) {
ch <- result{addr, pingSingle(addr)}
}(ip)
}
for range ips {
r := <-ch
results[r.ip] = r.ts
}
return results
}
// httpCheck performs an HTTP HEAD request and returns true if successful.
func httpCheck(url string) bool {
client := &http.Client{Timeout: defaultDetectTimeout}
start := time.Now()
resp, err := client.Head(url)
if err != nil {
// Fallback to GET
resp, err = client.Get(url)
if err != nil {
return false
}
}
defer resp.Body.Close()
_ = start
return resp.StatusCode < 500
}
// httpMulti checks multiple HTTP URLs concurrently and returns response times.
func httpMulti(urls []string) map[string]float64 {
results := make(map[string]float64)
type result struct {
url string
ts float64
}
ch := make(chan result, len(urls))
timeout := float64(defaultDetectTimeout.Seconds())
for _, url := range urls {
go func(addr string) {
start := time.Now()
client := &http.Client{Timeout: defaultDetectTimeout}
resp, err := client.Head(addr)
if err != nil {
// Fallback to GET
resp, err = client.Get(addr)
}
if err != nil || resp.StatusCode >= 500 {
ch <- result{addr, timeout}
return
}
resp.Body.Close()
ch <- result{addr, time.Since(start).Seconds()}
}(url)
}
for range urls {
r := <-ch
results[r.url] = r.ts
}
return results
}
// ============================================================================
// Network interface detection
// ============================================================================
// getNetworkInterfaces returns all non-loopback, non-docker IPv4 interfaces.
func getNetworkInterfaces() map[string]*NetworkInfo {
results := make(map[string]*NetworkInfo)
ifaces, err := net.Interfaces()
if err != nil {
log.Printf("[network] get interfaces error: %v", err)
return results
}
for _, iface := range ifaces {
name := iface.Name
// Skip loopback, docker, hassio interfaces
if name == "lo" || name == "hassio" || strings.HasPrefix(name, "docker") {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip := ipNet.IP
// IPv4 only, skip loopback
if ip.To4() == nil || ip.IsLoopback() {
continue
}
mask := net.IP(ipNet.Mask)
netSeg := ip.Mask(ipNet.Mask)
results[name] = &NetworkInfo{
Name: name,
IP: ip.String(),
Netmask: fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]),
NetSeg: netSeg.String(),
}
break // only first valid IPv4 per interface
}
}
return results
}
// UpdateAddrList updates the ping and HTTP detection address lists.
func (n *MIoTNetwork) UpdateAddrList(ipAddrList, urlAddrList []string) {
n.mu.Lock()
defer n.mu.Unlock()
if ipAddrList != nil {
newIPMap := make(map[string]float64)
for _, ip := range ipAddrList {
if ts, ok := n.ipAddrMap[ip]; ok {
newIPMap[ip] = ts
} else {
newIPMap[ip] = float64(defaultDetectTimeout.Seconds())
}
}
n.ipAddrMap = newIPMap
}
if urlAddrList != nil {
newURLMap := make(map[string]float64)
for _, url := range urlAddrList {
if ts, ok := n.httpAddrMap[url]; ok {
newURLMap[url] = ts
} else {
newURLMap[url] = float64(defaultDetectTimeout.Seconds())
}
}
n.httpAddrMap = newURLMap
}
}