github.com/simpleiot/simpleiot@v0.18.3/client/shelly.go (about) 1 package client 2 3 import ( 4 "log" 5 "regexp" 6 "strconv" 7 "time" 8 9 "github.com/google/uuid" 10 "github.com/nats-io/nats.go" 11 "github.com/simpleiot/mdns" 12 "github.com/simpleiot/simpleiot/data" 13 ) 14 15 // Shelly describes the shelly client config 16 type Shelly struct { 17 ID string `node:"id"` 18 Parent string `node:"parent"` 19 Description string `point:"description"` 20 Disabled bool `point:"disabled"` 21 IOs []ShellyIo `child:"shellyIo"` 22 } 23 24 // ShellyClient is a SIOT particle client 25 type ShellyClient struct { 26 nc *nats.Conn 27 config Shelly 28 stop chan struct{} 29 newPoints chan NewPoints 30 newEdgePoints chan NewPoints 31 newShellyPoints chan NewPoints 32 } 33 34 // NewShellyClient ... 35 func NewShellyClient(nc *nats.Conn, config Shelly) Client { 36 return &ShellyClient{ 37 nc: nc, 38 config: config, 39 stop: make(chan struct{}), 40 newPoints: make(chan NewPoints), 41 newEdgePoints: make(chan NewPoints), 42 newShellyPoints: make(chan NewPoints), 43 } 44 } 45 46 // Run runs the main logic for this client and blocks until stopped 47 func (sc *ShellyClient) Run() error { 48 log.Println("Starting shelly client:", sc.config.Description) 49 50 entriesCh := make(chan *mdns.ServiceEntry, 4) 51 52 params := mdns.DefaultParams("_http._tcp") 53 params.DisableIPv6 = true 54 params.Entries = entriesCh 55 56 scan := func() { 57 err := mdns.Query(params) 58 if err != nil { 59 log.Println("mdns error:", err) 60 } 61 } 62 63 go scan() 64 65 scanTicker := time.NewTicker(time.Minute * 1) 66 67 done: 68 for { 69 select { 70 case <-sc.stop: 71 log.Println("Stopping shelly client:", sc.config.Description) 72 break done 73 case pts := <-sc.newPoints: 74 err := data.MergePoints(pts.ID, pts.Points, &sc.config) 75 if err != nil { 76 log.Println("error merging new points:", err) 77 } 78 79 for _, p := range pts.Points { 80 switch p.Type { 81 case data.PointTypeDisabled: 82 } 83 } 84 85 case pts := <-sc.newEdgePoints: 86 err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &sc.config) 87 if err != nil { 88 log.Println("error merging new points:", err) 89 } 90 91 case <-scanTicker.C: 92 go scan() 93 94 case e := <-entriesCh: 95 typ, id := shellyScanHost(e.Host) 96 if len(typ) > 0 { 97 found := false 98 99 var ip string 100 if e.AddrV4 != nil { 101 ip = e.AddrV4.String() 102 } else if e.AddrV6 != nil { 103 ip = e.AddrV6.String() 104 } 105 106 for i, io := range sc.config.IOs { 107 if io.DeviceID == id { 108 // already have this one 109 // must set Origin because we are sending a point to another node 110 // if we don't set origin, then the client manager will filter out 111 // points to the client that owns the node 112 found = true 113 if io.IP != ip { 114 err := SendNodePoint(sc.nc, io.ID, data.Point{ 115 Type: data.PointTypeIP, 116 Text: ip, 117 Origin: sc.config.ID, 118 }, false) 119 120 if err != nil { 121 log.Println("Error setting io ip:", err) 122 } 123 } 124 125 if io.Offline { 126 err := SendNodePoint(sc.nc, io.ID, data.Point{ 127 Type: data.PointTypeOffline, 128 Value: 0, 129 Origin: sc.config.ID, 130 }, false) 131 132 if err != nil { 133 log.Println("Error setting io offline:", err) 134 } else { 135 sc.config.IOs[i].Offline = false 136 } 137 } 138 break 139 } 140 } 141 if found { 142 break 143 } 144 145 newIO := ShellyIo{ 146 ID: uuid.New().String(), 147 DeviceID: id, 148 Parent: sc.config.ID, 149 Type: typ, 150 IP: ip, 151 } 152 153 ne, err := data.Encode(newIO) 154 if err != nil { 155 log.Println("Error encoding new shelly IO:", err) 156 continue 157 } 158 159 addCompPoints := func(pType string, count int) { 160 for i := 0; i < count; i++ { 161 iString := strconv.Itoa(i) 162 ne.Points = append(ne.Points, data.Point{Type: pType, Key: iString}) 163 } 164 } 165 166 for _, comp := range shellyCompMap[typ] { 167 switch comp.name { 168 case "input": 169 addCompPoints(data.PointTypeInput, comp.count) 170 case "switch": 171 addCompPoints(data.PointTypeSwitch, comp.count) 172 addCompPoints(data.PointTypeSwitchSet, comp.count) 173 case "light": 174 addCompPoints(data.PointTypeLight, comp.count) 175 addCompPoints(data.PointTypeLightSet, comp.count) 176 } 177 } 178 179 err = SendNode(sc.nc, ne, sc.config.ID) 180 if err != nil { 181 log.Println("Error sending shelly IO:", err) 182 } 183 } 184 } 185 } 186 187 // clean up 188 scanTicker.Stop() 189 return nil 190 } 191 192 // Stop sends a signal to the Run function to exit 193 func (sc *ShellyClient) Stop(_ error) { 194 close(sc.stop) 195 } 196 197 // Points is called by the Manager when new points for this 198 // node are received. 199 func (sc *ShellyClient) Points(nodeID string, points []data.Point) { 200 sc.newPoints <- NewPoints{nodeID, "", points} 201 } 202 203 // EdgePoints is called by the Manager when new edge points for this 204 // node are received. 205 func (sc *ShellyClient) EdgePoints(nodeID, parentID string, points []data.Point) { 206 sc.newEdgePoints <- NewPoints{nodeID, parentID, points} 207 } 208 209 var reShellyHost = regexp.MustCompile("(?i)shelly(.*)-(.*).local") 210 211 func shellyScanHost(host string) (string, string) { 212 m := reShellyHost.FindStringSubmatch(host) 213 if len(m) < 3 { 214 return "", "" 215 } 216 217 return m[1], m[2] 218 }