github.com/simpleiot/simpleiot@v0.18.3/client/shelly-io-client.go (about) 1 package client 2 3 import ( 4 "fmt" 5 "log" 6 "time" 7 8 "github.com/nats-io/nats.go" 9 "github.com/simpleiot/simpleiot/data" 10 "golang.org/x/exp/constraints" 11 ) 12 13 // ShellyIOClient is a SIOT particle client 14 type ShellyIOClient struct { 15 nc *nats.Conn 16 config ShellyIo 17 points data.Points 18 stop chan struct{} 19 newPoints chan NewPoints 20 newEdgePoints chan NewPoints 21 newShellyPoints chan NewPoints 22 errorCount int 23 comps []shellyComp 24 } 25 26 // NewShellyIOClient ... 27 func NewShellyIOClient(nc *nats.Conn, config ShellyIo) Client { 28 // we need a copy of points with timestamps so we know when to send up new data 29 ne, err := data.Encode(config) 30 if err != nil { 31 log.Println("Error encoding shelly config:", err) 32 } 33 34 return &ShellyIOClient{ 35 nc: nc, 36 config: config, 37 comps: shellyCompMap[config.Type], 38 points: ne.Points, 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 (sioc *ShellyIOClient) Run() error { 48 log.Println("Starting shelly IO client:", sioc.config.Description) 49 50 sampleRate := time.Second * 2 51 sampleRateOffline := time.Minute * 10 52 53 syncConfigTicker := time.NewTicker(sampleRateOffline) 54 sampleTicker := time.NewTicker(sampleRate) 55 56 if sioc.config.Offline { 57 sampleTicker = time.NewTicker(sampleRateOffline) 58 } 59 60 if sioc.config.Disabled { 61 sampleTicker.Stop() 62 } 63 64 shellyError := func() { 65 sioc.errorCount++ 66 if !sioc.config.Offline && sioc.errorCount > 5 { 67 log.Printf("Shelly device %v is offline", sioc.config.Description) 68 sioc.config.Offline = true 69 err := SendNodePoint(sioc.nc, sioc.config.ID, data.Point{ 70 Type: data.PointTypeOffline, Value: 1}, false) 71 72 if err != nil { 73 log.Println("ShellyIO: error sending node point:", err) 74 } 75 sampleTicker = time.NewTicker(sampleRateOffline) 76 } 77 } 78 79 shellyCommOK := func() { 80 sioc.errorCount = 0 81 if sioc.config.Offline { 82 log.Printf("Shelly device %v is online", sioc.config.Description) 83 sioc.config.Offline = false 84 err := SendNodePoint(sioc.nc, sioc.config.ID, data.Point{ 85 Type: data.PointTypeOffline, Value: 0}, false) 86 87 if err != nil { 88 log.Println("ShellyIO: error sending node point:", err) 89 } 90 sampleTicker = time.NewTicker(sampleRate) 91 } 92 } 93 94 syncConfig := func() { 95 config, err := sioc.config.getConfig() 96 if err != nil { 97 shellyError() 98 log.Println("Error getting shelly IO settings:", sioc.config.Desc(), err) 99 return 100 } 101 102 shellyCommOK() 103 104 if sioc.config.Description == "" && config.Name != "" { 105 sioc.config.Description = config.Name 106 err := SendNodePoint(sioc.nc, sioc.config.ID, data.Point{ 107 Type: data.PointTypeDescription, Text: config.Name}, false) 108 if err != nil { 109 log.Println("Error sending shelly io description:", err) 110 } 111 } else if sioc.config.Description != config.Name { 112 err := sioc.config.SetName(sioc.config.Description) 113 if err != nil { 114 log.Println("Error setting name on Shelly device:", err) 115 } 116 } 117 } 118 119 syncConfig() 120 121 done: 122 for { 123 select { 124 case <-sioc.stop: 125 log.Println("Stopping shelly IO client:", sioc.config.Description) 126 break done 127 case pts := <-sioc.newPoints: 128 err := data.MergePoints(pts.ID, pts.Points, &sioc.config) 129 if err != nil { 130 log.Println("error merging new points:", err) 131 } 132 133 for _, p := range pts.Points { 134 switch p.Type { 135 case data.PointTypeDescription: 136 syncConfig() 137 case data.PointTypeDisabled: 138 if p.Value == 0 { 139 sampleTicker = time.NewTicker(sampleRate) 140 } else { 141 sampleTicker.Stop() 142 } 143 case data.PointTypeOffline: 144 if p.Value == 0 { 145 // defice is online 146 // the discovery mechanism may have set the IO back online 147 sampleTicker = time.NewTicker(sampleRate) 148 } else { 149 sampleTicker = time.NewTicker(sampleRateOffline) 150 } 151 } 152 } 153 154 case pts := <-sioc.newEdgePoints: 155 err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &sioc.config) 156 if err != nil { 157 log.Println("error merging new points:", err) 158 } 159 160 case <-syncConfigTicker.C: 161 syncConfig() 162 163 case <-sampleTicker.C: 164 if sioc.config.Disabled { 165 fmt.Println("Shelly IO is disabled, why am I ticking?") 166 continue 167 } 168 points, err := sioc.config.GetStatus() 169 if err != nil { 170 log.Printf("Error getting status for %v: %v\n", sioc.config.Description, err) 171 shellyError() 172 break 173 } 174 175 if sioc.config.Controlled { 176 switchCount := min(len(sioc.config.Switch), len(sioc.config.SwitchSet)) 177 for i := 0; i < switchCount; i++ { 178 if sioc.config.Switch[i] != sioc.config.SwitchSet[i] { 179 pts, err := sioc.config.SetOnOff("switch", i, sioc.config.SwitchSet[i]) 180 if err != nil { 181 log.Printf("Error setting %v: %v\n", sioc.config.Description, err) 182 } 183 184 if len(pts) > 0 { 185 points = append(points, pts...) 186 } else { 187 // get current status as the set did not return status 188 points, err = sioc.config.GetStatus() 189 if err != nil { 190 log.Printf("Error getting status for %v: %v\n", sioc.config.Description, err) 191 shellyError() 192 break 193 } 194 } 195 } 196 } 197 198 lightCount := min(len(sioc.config.Light), len(sioc.config.LightSet)) 199 for i := 0; i < lightCount; i++ { 200 if sioc.config.Light[i] != sioc.config.LightSet[i] { 201 pts, err := sioc.config.SetOnOff("light", i, sioc.config.LightSet[i]) 202 if err != nil { 203 log.Printf("Error setting %v: %v\n", sioc.config.Description, err) 204 } 205 206 if len(pts) > 0 { 207 points = append(points, pts...) 208 } else { 209 // get current status as the set did not return status 210 points, err = sioc.config.GetStatus() 211 if err != nil { 212 log.Printf("Error getting status for %v: %v\n", sioc.config.Description, err) 213 shellyError() 214 break 215 } 216 } 217 } 218 } 219 220 } 221 222 shellyCommOK() 223 224 newPoints := sioc.points.Merge(points, time.Minute*15) 225 if len(newPoints) > 0 { 226 err := data.MergePoints(sioc.config.ID, newPoints, &sioc.config) 227 if err != nil { 228 log.Println("shelly io: error merging newPoints:", err) 229 } 230 err = SendNodePoints(sioc.nc, sioc.config.ID, newPoints, false) 231 if err != nil { 232 log.Println("shelly io: error sending newPoints:", err) 233 } 234 } 235 } 236 } 237 238 // clean up 239 return nil 240 } 241 242 // Stop sends a signal to the Run function to exit 243 func (sioc *ShellyIOClient) Stop(_ error) { 244 close(sioc.stop) 245 } 246 247 // Points is called by the Manager when new points for this 248 // node are received. 249 func (sioc *ShellyIOClient) Points(nodeID string, points []data.Point) { 250 sioc.newPoints <- NewPoints{nodeID, "", points} 251 } 252 253 // EdgePoints is called by the Manager when new edge points for this 254 // node are received. 255 func (sioc *ShellyIOClient) EdgePoints(nodeID, parentID string, points []data.Point) { 256 sioc.newEdgePoints <- NewPoints{nodeID, parentID, points} 257 } 258 259 func min[T constraints.Ordered](a, b T) T { 260 if a < b { 261 return a 262 } 263 return b 264 }