@@ -13,8 +13,7 @@ import (
1313 "net/http"
1414 "time"
1515
16- "github.com/coder/websocket"
17- // "github.com/coder/websocket/wsjson"
16+ "github.com/gorilla/websocket"
1817 "github.com/sdoque/mbaigo/components"
1918 "github.com/sdoque/mbaigo/forms"
2019 "github.com/sdoque/mbaigo/usecases"
@@ -45,6 +44,7 @@ type UnitAsset struct {
4544 deviceIndex string
4645 Period time.Duration `json:"period"`
4746 Setpt float64 `json:"setpoint"`
47+ Slaves []string `json:"slaves"`
4848 Apikey string `json:"APIkey"`
4949}
5050
@@ -98,6 +98,7 @@ func initTemplate() components.UnitAsset {
9898 deviceIndex : "" ,
9999 Period : 10 ,
100100 Setpt : 20 ,
101+ Slaves : []string {},
101102 Apikey : "1234" ,
102103 ServicesMap : components.Services {
103104 setPointService .SubPath : & setPointService ,
@@ -132,6 +133,7 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi
132133 deviceIndex : uac .deviceIndex ,
133134 Period : uac .Period ,
134135 Setpt : uac .Setpt ,
136+ Slaves : uac .Slaves ,
135137 Apikey : uac .Apikey ,
136138 CervicesMap : components.Cervices {
137139 t .Name : t ,
@@ -146,33 +148,26 @@ func newResource(uac UnitAsset, sys *components.System, servs []components.Servi
146148 ua .CervicesMap ["temperature" ].Details = components .MergeDetails (ua .Details , ref .Details )
147149
148150 return ua , func () {
149- if ua .Model == "ZHAThermostat" {
150- /*
151- // Get correct index in list returned by api/sensors to make sure we always change correct device
152- err := ua.getConnectedUnits("sensors")
153- if err != nil {
154- log.Println("Error occured during startup, while calling getConnectedUnits:", err)
155- }
156- */
151+ if websocketport == "startup" {
152+ ua .getWebsocketPort ()
153+ }
154+ switch ua .Model {
155+ case "ZHAThermostat" :
157156 err := ua .sendSetPoint ()
158157 if err != nil {
159158 log .Println ("Error occured during startup, while calling sendSetPoint():" , err )
160- // TODO: Turn off system if this startup() fails?
161159 }
162- } else if ua .Model == "Smart plug" {
163- /*
164- // Get correct index in list returned by api/lights to make sure we always change correct device
165- err := ua.getConnectedUnits("lights")
166- if err != nil {
167- log.Println("Error occured during startup, while calling getConnectedUnits:", err)
168- }
169- */
160+ case "Smart plug" :
170161 // Not all smart plugs should be handled by the feedbackloop, some should be handled by a switch
171- if ua .Period != 0 {
162+ if ua .Period > 0 {
172163 // start the unit assets feedbackloop, this fetches the temperature from ds18b20 and and toggles
173164 // between on/off depending on temperature in the room and a set temperature in the unitasset
174165 go ua .feedbackLoop (ua .Owner .Ctx )
175166 }
167+ case "ZHASwitch" :
168+ // Starts listening to the websocket to find buttonevents (button presses) and then
169+ // turns its controlled devices on/off
170+ go ua .initWebsocketClient (ua .Owner .Ctx )
176171 }
177172 }
178173}
@@ -301,7 +296,6 @@ func (ua *UnitAsset) toggleState(state bool) (err error) {
301296}
302297
303298// Useless function? Noticed uniqueid can be used as "id" to send requests instead of the index while testing, wasn't clear from documentation. Will need to test this more though
304- // TODO: Rewrite this to instead get the websocketport.
305299func (ua * UnitAsset ) getConnectedUnits (unitType string ) (err error ) {
306300 // --- Get all devices ---
307301 apiURL := fmt .Sprintf ("http://%s/api/%s/%s" , gateway , ua .Apikey , unitType )
@@ -369,40 +363,112 @@ func sendRequest(req *http.Request) (err error) {
369363// Port 443, can be found by curl -v "http://localhost:8080/api/[apikey]/config", and getting the "websocketport". Will make a function to automatically get this port
370364// https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/
371365// https://stackoverflow.com/questions/32745716/i-need-to-connect-to-an-existing-websocket-server-using-go-lang
372- // https://pkg.go.dev/github.com/coder/websocket#Dial
373- // https://pkg.go.dev/github.com/coder/websocket#Conn.Reader
374-
375- // Not sure if this will work, still a work in progress.
376- func initWebsocketClient (ctx context.Context ) (err error ) {
377- fmt .Println ("Starting Client" )
378- ws , _ , err := websocket .Dial (ctx , "ws://localhost:443" , nil ) // Start listening to websocket
379- defer ws .CloseNow () // Make sure connection is closed when returning from function
366+ // https://github.com/gorilla/websocket
367+
368+ var websocketport = "startup"
369+
370+ type eventJSON struct {
371+ State struct {
372+ Buttonevent int `json:"buttonevent"`
373+ } `json:"state"`
374+ UniqueID string `json:"uniqueid"`
375+ }
376+
377+ func (ua * UnitAsset ) getWebsocketPort () (err error ) {
378+ // --- Get config ---
379+ apiURL := fmt .Sprintf ("http://%s/api/%s/config" , gateway , ua .Apikey )
380+ // Create a new request (Get)
381+ // Put data into buffer
382+ req , err := http .NewRequest (http .MethodGet , apiURL , nil ) // Put request is made
383+ req .Header .Set ("Content-Type" , "application/json" ) // Make sure it's JSON
384+ // Send the request
385+ resp , err := http .DefaultClient .Do (req ) // Perform the http request
380386 if err != nil {
381- fmt .Printf ("Dial failed: %s\n " , err )
382387 return err
383388 }
384- _ , body , err := ws .Reader (ctx ) // Start reading from connection, returned body will be used to get buttonevents
389+ defer resp .Body .Close ()
390+ resBody , err := io .ReadAll (resp .Body ) // Read the response body, and check for errors/bad statuscodes
385391 if err != nil {
386- log .Println ("Error while reading from websocket:" , err )
387- return
392+ return err
388393 }
389- data , err := io .ReadAll (body )
390- if err != nil {
391- log .Println ("Error while converthing from io.Reader to []byte:" , err )
392- return
394+ if resp .StatusCode > 299 {
395+ return errStatusCode
393396 }
394- var bodyString map [string ]interface {}
395- err = json .Unmarshal (data , & bodyString ) // Unmarshal body into json, easier to be able to point to specific data with ".example"
397+ // How to access maps inside of maps below!
398+ // https://stackoverflow.com/questions/28806951/accessing-nested-map-of-type-mapstringinterface-in-golang
399+ var configMap map [string ]interface {}
400+ err = json .Unmarshal ([]byte (resBody ), & configMap )
396401 if err != nil {
397- log .Println ("Error while unmarshaling data:" , err )
398- return
402+ return err
399403 }
400- log .Println ("Read from websocket:" , bodyString )
401- err = ws .Close (websocket .StatusNormalClosure , "No longer need to listen to websocket" )
404+ websocketport = fmt .Sprint (configMap ["websocketport" ])
405+ // log.Println(configMap["websocketport"])
406+ return
407+ }
408+
409+ func (ua * UnitAsset ) toggleSlaves (currentState bool ) (err error ) {
410+ for i := range ua .Slaves {
411+ // Add check if current slave is smart plug or a light, like philips hue
412+
413+ // API call to toggle smart plug on/off, PUT call should be sent to URL/api/apikey/lights/sensor_id/config
414+ apiURL := fmt .Sprintf ("http://%s/api/%s/lights/%s/state" , gateway , ua .Apikey , ua .Slaves [i ])
415+ // Create http friendly payload
416+ s := fmt .Sprintf (`{"on":%t}` , currentState ) // Create payload
417+ req , err := createRequest (s , apiURL )
418+ if err != nil {
419+ return err
420+ }
421+ sendRequest (req )
422+ }
423+ return err
424+ }
425+
426+ // Function starts listening to a websocket, every message received through websocket is read, and checked if it's what we're looking for
427+ // The uniqueid (UniqueID in systemconfig.json file) from the connected switch is used to filter out messages
428+ func (ua * UnitAsset ) initWebsocketClient (ctx context.Context ) error {
429+ dialer := websocket.Dialer {}
430+ wsURL := fmt .Sprintf ("ws://localhost:%s" , websocketport )
431+ conn , _ , err := dialer .Dial (wsURL , nil )
402432 if err != nil {
403- log .Println ("Error while doing normal closure on websocket" )
404- return
433+ log .Fatal ("Error occured while dialing:" , err )
434+ }
435+ log .Println ("Connected to websocket" )
436+ defer conn .Close ()
437+ currentState := false
438+ log .Println (currentState )
439+ for {
440+ select {
441+ case <- ctx .Done ():
442+ return nil
443+ default :
444+ _ , p , err := conn .ReadMessage ()
445+ if err != nil {
446+ log .Println ("Error occured while reading message:" , err )
447+ return err
448+ }
449+ var message eventJSON
450+ //var message interface{}
451+ err = json .Unmarshal (p , & message )
452+ if err != nil {
453+ log .Println ("Error unmarshalling message:" , err )
454+ return err
455+ }
456+
457+ if message .UniqueID == ua .Uniqueid && (message .State .Buttonevent == 1002 || message .State .Buttonevent == 2002 ) {
458+ bEvent := message .State .Buttonevent
459+ if bEvent == 1002 {
460+ if currentState == true {
461+ currentState = false
462+ } else {
463+ currentState = true
464+ }
465+ ua .toggleSlaves (currentState )
466+ }
467+ if bEvent == 2002 {
468+ // Turn on the philips hue light
469+ // TODO: Find out how "long presses" works and if it can be used through websocket
470+ }
471+ }
472+ }
405473 }
406- return
407- // Have to do something fancy to make sure we update "connected" plugs/lights when Reader returns a body actually containing a buttonevent (something w/ channels?)
408474}
0 commit comments