feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user