github.com/simpleiot/simpleiot@v0.18.3/client/shelly-io.go (about) 1 package client 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/simpleiot/simpleiot/data" 12 ) 13 14 // shellyIOConfig describes the configuration of a Shelly device 15 type shellyIOConfig struct { 16 Name string `json:"name"` 17 } 18 19 type shellyGen2SysConfig struct { 20 Device struct { 21 Name string `json:"name"` 22 } `json:"device"` 23 } 24 25 // Example response 26 // {"id":0, "source":"WS_in", "output":false, "apower":0.0, "voltage":123.3, "current":0.000, "aenergy":{"total":0.000,"by_minute":[0.000,0.000,0.000],"minute_ts":1680536525},"temperature":{"tC":44.4, "tF":112.0}} 27 type shellyGen2SwitchStatus struct { 28 ID int `json:"id"` 29 Source string `json:"source"` 30 Output bool `json:"output"` 31 Apower float32 `json:"apower"` 32 Voltage float32 `json:"voltage"` 33 Current float32 `json:"current"` 34 Aenergy struct { 35 Total float32 `json:"total"` 36 ByMinute []float32 `json:"by_minute"` 37 MinuteTS int64 `json:"minute_ts"` 38 } `json:"aenergy"` 39 Temperature struct { 40 TC float32 `json:"tC"` 41 TF float32 `json:"tF"` 42 } `json:"temperature"` 43 } 44 45 type shellyGen2SwitchSetResp struct { 46 WasOn bool `json:"wasOn"` 47 } 48 49 func (swi *shellyGen2SwitchStatus) toPoints(index int) data.Points { 50 now := time.Now() 51 key := strconv.Itoa(index) 52 return data.Points{ 53 {Time: now, Type: data.PointTypeSwitch, Key: key, Value: data.BoolToFloat(swi.Output)}, 54 {Time: now, Type: data.PointTypePower, Key: key, Value: float64(swi.Apower)}, 55 {Time: now, Type: data.PointTypeVoltage, Key: key, Value: float64(swi.Voltage)}, 56 {Time: now, Type: data.PointTypeCurrent, Key: key, Value: float64(swi.Current)}, 57 {Time: now, Type: data.PointTypeTemperature, Key: key, Value: float64(swi.Temperature.TC)}, 58 } 59 } 60 61 // Example response 62 // {"id":2,"state":true} 63 type shellyGen2InputStatus struct { 64 ID int `json:"id"` 65 State bool `json:"state"` 66 } 67 68 func (in *shellyGen2InputStatus) toPoints() data.Points { 69 now := time.Now() 70 return data.Points{ 71 {Time: now, Type: data.PointTypeInput, 72 Key: strconv.Itoa(in.ID), 73 Value: data.BoolToFloat(in.State)}, 74 } 75 } 76 77 type shellyGen1LightStatus struct { 78 Ison bool `json:"ison"` 79 Brightness int `json:"brightness"` 80 White int `json:"white"` 81 Temp int `json:"temp"` 82 Transition int `json:"transition"` 83 } 84 85 func (sls *shellyGen1LightStatus) toPoints() data.Points { 86 now := time.Now() 87 return data.Points{ 88 {Time: now, Type: data.PointTypeLight, Key: "0", Value: data.BoolToFloat(sls.Ison)}, 89 {Time: now, Type: data.PointTypeBrightness, Key: "0", Value: float64(sls.Brightness)}, 90 {Time: now, Type: data.PointTypeWhite, Key: "0", Value: float64(sls.White)}, 91 {Time: now, Type: data.PointTypeLightTemp, Key: "0", Value: float64(sls.Temp)}, 92 {Time: now, Type: data.PointTypeTransition, Key: "0", Value: float64(sls.Transition)}, 93 } 94 } 95 96 func (sg2c shellyGen2SysConfig) toSettings() shellyIOConfig { 97 return shellyIOConfig{ 98 Name: sg2c.Device.Name, 99 } 100 } 101 102 // ShellyIo describes the config/state for a shelly io 103 type ShellyIo struct { 104 ID string `node:"id"` 105 Parent string `node:"parent"` 106 Description string `point:"description"` 107 DeviceID string `point:"deviceID"` 108 Type string `point:"type"` 109 IP string `point:"ip"` 110 Value []float64 `point:"value"` 111 ValueSet []float64 `point:"valueSet"` 112 Switch []bool `point:"switch"` 113 SwitchSet []bool `point:"switchSet"` 114 Light []bool `point:"light"` 115 LightSet []bool `point:"lightSet"` 116 Input []bool `point:"input"` 117 Offline bool `point:"offline"` 118 Controlled bool `point:"controlled"` 119 Disabled bool `point:"disabled"` 120 } 121 122 // Desc gets the description of a Shelly IO 123 func (sio *ShellyIo) Desc() string { 124 ret := sio.Type 125 if len(sio.Description) > 0 { 126 ret += ":" + sio.Description 127 } 128 return ret 129 } 130 131 var httpClient = &http.Client{Timeout: 10 * time.Second} 132 133 // ShellyGen describes the generation of device (Gen1/Gen2) 134 type ShellyGen int 135 136 // Shelly Generations 137 const ( 138 ShellyGenUnknown ShellyGen = iota 139 ShellyGen1 140 ShellyGen2 141 ) 142 143 var shellyGenMap = map[string]ShellyGen{ 144 data.PointValueShellyTypeBulbDuo: ShellyGen1, 145 data.PointValueShellyTypeRGBW2: ShellyGen1, 146 data.PointValueShellyType1PM: ShellyGen1, 147 data.PointValueShellyTypePlugUS: ShellyGen2, 148 data.PointValueShellyTypePlugUK: ShellyGen2, 149 data.PointValueShellyTypePlugIT: ShellyGen2, 150 data.PointValueShellyTypePlugS: ShellyGen2, 151 data.PointValueShellyTypeI4: ShellyGen2, 152 data.PointValueShellyTypePlus1: ShellyGen2, 153 data.PointValueShellyTypePlus2PM: ShellyGen2, 154 } 155 156 // Gen 2 metadata 157 158 // shellComp is used to describe shelly "components" a device may support 159 type shellyComp struct { 160 name string 161 count int 162 } 163 164 var shellyCompMap = map[string][]shellyComp{ 165 data.PointValueShellyTypeBulbDuo: {{"light", 1}}, 166 data.PointValueShellyType1PM: {{"switch", 1}}, 167 data.PointValueShellyTypeI4: {{"input", 4}}, 168 data.PointValueShellyTypePlugUS: {{"switch", 1}}, 169 data.PointValueShellyTypePlugUK: {{"switch", 1}}, 170 data.PointValueShellyTypePlugIT: {{"switch", 1}}, 171 data.PointValueShellyTypePlugS: {{"switch", 1}}, 172 data.PointValueShellyTypePlus1: {{"switch", 1}, {"input", 1}}, 173 data.PointValueShellyTypePlus2PM: {{"switch", 2}, {"input", 2}}, 174 } 175 176 var shellySettableOnOff = map[string]bool{ 177 data.PointValueShellyTypeBulbDuo: true, 178 data.PointValueShellyTypeRGBW2: true, 179 data.PointValueShellyType1PM: true, 180 data.PointValueShellyTypePlugUS: true, 181 data.PointValueShellyTypePlugUK: true, 182 data.PointValueShellyTypePlugIT: true, 183 data.PointValueShellyTypePlugS: true, 184 data.PointValueShellyTypePlus1: true, 185 data.PointValueShellyTypePlus2PM: true, 186 } 187 188 // Gen returns generation of Shelly device 189 func (sio *ShellyIo) Gen() ShellyGen { 190 gen, ok := shellyGenMap[sio.Type] 191 if !ok { 192 return ShellyGenUnknown 193 } 194 195 return gen 196 } 197 198 // IsSettableOnOff returns true if the device can be turned on/off 199 func (sio *ShellyIo) IsSettableOnOff() bool { 200 settable := shellySettableOnOff[sio.Type] 201 return settable 202 } 203 204 // GetConfig returns the configuration of Shelly Device 205 func (sio *ShellyIo) getConfig() (shellyIOConfig, error) { 206 switch sio.Gen() { 207 case ShellyGen1: 208 var ret shellyIOConfig 209 res, err := httpClient.Get("http://" + sio.IP + "/settings") 210 if err != nil { 211 return ret, err 212 } 213 defer res.Body.Close() 214 if res.StatusCode != http.StatusOK { 215 return ret, fmt.Errorf("Shelly GetConfig returned an error code: %v", res.StatusCode) 216 } 217 218 err = json.NewDecoder(res.Body).Decode(&ret) 219 220 return ret, err 221 case ShellyGen2: 222 var config shellyGen2SysConfig 223 res, err := httpClient.Get("http://" + sio.IP + "/rpc/Sys.GetConfig") 224 if err != nil { 225 return config.toSettings(), err 226 } 227 defer res.Body.Close() 228 if res.StatusCode != http.StatusOK { 229 return config.toSettings(), fmt.Errorf("Shelly GetConfig returned an error code: %v", res.StatusCode) 230 } 231 232 err = json.NewDecoder(res.Body).Decode(&config) 233 return config.toSettings(), err 234 235 default: 236 return shellyIOConfig{}, fmt.Errorf("Unsupported device: %v", sio.Type) 237 } 238 } 239 240 // SetOnOff sets on/off state of device 241 // BulbDuo: http://10.0.0.130/light/0?turn=on 242 // PlugUS: http://192.168.33.1/rpc/Switch.Set?id=0&on=true 243 func (sio *ShellyIo) SetOnOff(comp string, index int, on bool) (data.Points, error) { 244 if len(comp) < 2 { 245 return nil, fmt.Errorf("Component must be specified") 246 } 247 _ = index 248 gen := sio.Gen() 249 switch gen { 250 case ShellyGen1: 251 onoff := "off" 252 if on { 253 onoff = "on" 254 } 255 url := fmt.Sprintf("http://%v/%v/%v?turn=%v", sio.IP, comp, index, onoff) 256 res, err := httpClient.Get(url) 257 if err != nil { 258 return data.Points{}, err 259 } 260 defer res.Body.Close() 261 if res.StatusCode != http.StatusOK { 262 return data.Points{}, fmt.Errorf("Shelly GetConfig returned an error code: %v", res.StatusCode) 263 } 264 265 var status shellyGen1LightStatus 266 267 err = json.NewDecoder(res.Body).Decode(&status) 268 if err != nil { 269 return data.Points{}, err 270 } 271 return status.toPoints(), nil 272 case ShellyGen2: 273 onValue := "false" 274 if on { 275 onValue = "true" 276 } 277 278 compCap := strings.ToUpper(string(comp[0])) + comp[1:] 279 280 url := fmt.Sprintf("http://%v/rpc/%v.Set?id=%v&on=%v", sio.IP, compCap, index, onValue) 281 res, err := httpClient.Get(url) 282 if err != nil { 283 return data.Points{}, err 284 } 285 defer res.Body.Close() 286 if res.StatusCode != http.StatusOK { 287 return data.Points{}, fmt.Errorf("Shelly Switch.Set returned an error code: %v", res.StatusCode) 288 } 289 290 var status shellyGen2SwitchSetResp 291 292 err = json.NewDecoder(res.Body).Decode(&status) 293 if err != nil { 294 return data.Points{}, err 295 } 296 return data.Points{}, nil 297 298 default: 299 return data.Points{}, nil 300 } 301 } 302 303 func (sio *ShellyIo) gen1GetLight(count int) (data.Points, error) { 304 ret := data.Points{} 305 306 for i := 0; i < count; i++ { 307 res, err := httpClient.Get("http://" + sio.IP + "/light/0") 308 if err != nil { 309 return data.Points{}, err 310 } 311 defer res.Body.Close() 312 if res.StatusCode != http.StatusOK { 313 return data.Points{}, fmt.Errorf("Shelly GetConfig returned an error code: %v", res.StatusCode) 314 } 315 316 var status shellyGen1LightStatus 317 318 err = json.NewDecoder(res.Body).Decode(&status) 319 if err != nil { 320 return data.Points{}, err 321 } 322 323 ret = append(ret, status.toPoints()...) 324 } 325 326 return ret, nil 327 } 328 329 func (sio *ShellyIo) gen2GetSwitch(count int) (data.Points, error) { 330 ret := data.Points{} 331 332 for i := 0; i < count; i++ { 333 334 url := fmt.Sprintf("http://%v/rpc/Switch.GetStatus?id=%v", sio.IP, i) 335 336 res, err := httpClient.Get(url) 337 if err != nil { 338 return data.Points{}, err 339 } 340 defer res.Body.Close() 341 if res.StatusCode != http.StatusOK { 342 return data.Points{}, fmt.Errorf("Shelly GetConfig returned an error code: %v", res.StatusCode) 343 } 344 345 var status shellyGen2SwitchStatus 346 347 err = json.NewDecoder(res.Body).Decode(&status) 348 if err != nil { 349 return data.Points{}, err 350 } 351 pts := status.toPoints(i) 352 ret = append(ret, pts...) 353 } 354 355 return ret, nil 356 } 357 358 func (sio *ShellyIo) gen2GetInput(count int) (data.Points, error) { 359 var points data.Points 360 for channel := 0; channel < count; channel++ { 361 res, err := httpClient.Get("http://" + sio.IP + "/rpc/Input.GetStatus?id=" + strconv.Itoa(channel)) 362 if err != nil { 363 return data.Points{}, err 364 } 365 defer res.Body.Close() 366 if res.StatusCode != http.StatusOK { 367 return data.Points{}, fmt.Errorf("Shelly GetConfig returned an error code: %v", res.StatusCode) 368 } 369 370 var status shellyGen2InputStatus 371 372 err = json.NewDecoder(res.Body).Decode(&status) 373 if err != nil { 374 return data.Points{}, err 375 } 376 377 points = append(points, status.toPoints()...) 378 } 379 380 return points, nil 381 } 382 383 // GetStatus gets the current status of the device 384 func (sio *ShellyIo) GetStatus() (data.Points, error) { 385 ret := data.Points{} 386 387 // TODO: this needs clean up some to be data driven instead of all the if statements 388 gen := sio.Gen() 389 390 if cnt := sio.getCompCount("switch"); cnt > 0 { 391 if gen == ShellyGen1 { 392 _ = gen 393 // TODO: need to add gen 1 support for switch status 394 } 395 if gen == ShellyGen2 { 396 pts, err := sio.gen2GetSwitch(cnt) 397 if err != nil { 398 return nil, err 399 } 400 ret = append(ret, pts...) 401 } 402 } 403 404 if cnt := sio.getCompCount("input"); cnt > 0 { 405 if gen == ShellyGen1 { 406 _ = gen 407 // TODO: need to add gen 1 support for input status 408 } 409 if gen == ShellyGen2 { 410 pts, err := sio.gen2GetInput(cnt) 411 if err != nil { 412 return nil, err 413 } 414 ret = append(ret, pts...) 415 } 416 } 417 418 if cnt := sio.getCompCount("light"); cnt > 0 { 419 if gen == ShellyGen1 { 420 pts, err := sio.gen1GetLight(cnt) 421 if err != nil { 422 return nil, err 423 } 424 ret = append(ret, pts...) 425 } 426 } 427 428 return ret, nil 429 } 430 431 type shellyGen2Response struct { 432 RestartRequired bool `json:"restartRequired"` 433 Code int `json:"code"` 434 Message string `json:"message"` 435 } 436 437 // SetName is use to set the name in a device 438 func (sio *ShellyIo) SetName(name string) error { 439 switch sio.Gen() { 440 case ShellyGen1: 441 uri := fmt.Sprintf("http://%v/settings?name=%v", sio.IP, name) 442 uri = strings.Replace(uri, " ", "%20", -1) 443 res, err := httpClient.Get(uri) 444 if err != nil { 445 return err 446 } 447 defer res.Body.Close() 448 if res.StatusCode != http.StatusOK { 449 return fmt.Errorf("Shelly SetName returned an error code: %v", res.StatusCode) 450 } 451 // TODO: not sure how to test if it worked ... 452 case ShellyGen2: 453 uri := fmt.Sprintf("http://%v/rpc/Sys.Setconfig?config={\"device\":{\"name\":\"%v\"}}", sio.IP, name) 454 uri = strings.Replace(uri, " ", "%20", -1) 455 res, err := httpClient.Get(uri) 456 if err != nil { 457 return err 458 } 459 defer res.Body.Close() 460 if res.StatusCode != http.StatusOK { 461 return fmt.Errorf("Shelly SetName returned an error code: %v", res.StatusCode) 462 } 463 var ret shellyGen2Response 464 err = json.NewDecoder(res.Body).Decode(&ret) 465 if err != nil { 466 return err 467 } 468 if ret.Code != 0 || ret.Message != "" { 469 return fmt.Errorf("Error setting Shelly device %v name: %v", sio.Type, ret.Message) 470 } 471 default: 472 return fmt.Errorf("Error setting name: Unsupported device: %v", sio.Type) 473 } 474 return nil 475 } 476 477 // GetCompCount returns the number of components found in the device 478 func (sio *ShellyIo) getCompCount(comp string) int { 479 comps, ok := shellyCompMap[sio.Type] 480 if !ok { 481 return 0 482 } 483 484 for _, c := range comps { 485 if c.name == comp { 486 return c.count 487 } 488 } 489 490 return 0 491 }