diff --git a/README.md b/README.md index e8557c5..bdf9a17 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/) integration d Currently this repository implements a driver for the following devices: * Denon Audio/Video Reveiver +* [DeCONZ](https://dresden-elektronik.github.io/deconz-rest-doc/) ## Device / Clients @@ -24,7 +25,9 @@ Run with `ucrt denon` This client currently implements a [`MediaPlayer` entity](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_media_player.md) and some [`Button` entities](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_button.md) +### Deconz +Run with `ucrt deconz` ## How to use diff --git a/go.mod b/go.mod index 683726c..cd76fbe 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/mdns v1.0.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jurgen-kluft/go-conbee v0.0.0-20211124004556-1d2ff903ea59 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/miekg/dns v1.1.56 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/go.sum b/go.sum index d16dd58..46ca264 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jurgen-kluft/go-conbee v0.0.0-20211124004556-1d2ff903ea59 h1:f9Gn7xzl2wfNdlJ4slukOmX8LhOs9XflRDbaQxA/Onk= +github.com/jurgen-kluft/go-conbee v0.0.0-20211124004556-1d2ff903ea59/go.mod h1:nxv2+SfQy+RF9UzE0rnRpYJGVmBA1MQ45UxUgiOxdqg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pkg/client/client.go b/pkg/client/client.go index e6bb167..dc4dc00 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -23,7 +23,8 @@ type Client struct { // Called by RemoteTwo when the integration is added and setup started setupFunc func() // Handles connect/disconnect calls from RemoteTwo - clientLoopFunc func() + clientLoopFunc func() + setDriverUserDataFunc func(map[string]string, bool) } func NewClient(i *integration.Integration) *Client { @@ -41,6 +42,13 @@ func NewClient(i *integration.Integration) *Client { } func (c *Client) InitClient() { + + // Pass function to the integration driver that is called when the remote want to setup the driver + c.IntegrationDriver.SetHandleSetupFunction(c.HandleSetup) + // Pass function to the integration driver that is called when the remote want to setup the driver + c.IntegrationDriver.SetHandleConnectionFunction(c.HandleConnection) + c.IntegrationDriver.SetHandleSetDriverUserDataFunction(c.HandleSetDriverUserDataFunction) + // Call setup Function if its set if c.initFunc != nil { c.initFunc() @@ -95,12 +103,8 @@ func (c *Client) HandleSetDriverUserDataFunction(userdata map[string]string, con "Confim": confirm, }).Debug(("Handle SetDriverUserData")) - if confirm { - c.IntegrationDriver.SetDriverSetupState(integration.StopEvent, integration.OkState, "", nil) - } else { - c.IntegrationDriver.SetDriverSetupState(integration.StopEvent, integration.OkState, "", nil) - // Confirm is not set.. Bug? - //c.IntegrationDriver.SetDriverSetupState(integration.SetupEvent, integration.WaitUserActionState, "", nil) + if c.setDriverUserDataFunc != nil { + c.setDriverUserDataFunc(userdata, confirm) } } diff --git a/pkg/client/deconzclient.go b/pkg/client/deconzclient.go new file mode 100644 index 0000000..d2336ae --- /dev/null +++ b/pkg/client/deconzclient.go @@ -0,0 +1,415 @@ +package client + +import ( + "fmt" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + "github.com/splattner/goucrt/pkg/deconz" + "github.com/splattner/goucrt/pkg/entities" + "github.com/splattner/goucrt/pkg/integration" +) + +// Denon AVR Client Implementation +type DeconzClient struct { + Client + deconz *deconz.Deconz +} + +func NewDeconzClient(i *integration.Integration) *DeconzClient { + client := DeconzClient{} + + client.IntegrationDriver = i + // Start without a connection + client.DeviceState = integration.DisconnectedDeviceState + + client.messages = make(chan string) + + ipaddr := integration.SetupDataSchemaSettings{ + Id: "ipaddr", + Label: integration.LanguageText{ + En: "IP Address of your deCONZ Client", + }, + Field: integration.SettingTypeText{ + Text: integration.SettingTypeTextDefinition{ + Value: "", + }, + }, + } + + port := integration.SetupDataSchemaSettings{ + Id: "port", + Label: integration.LanguageText{ + En: "Port used by your deCONZ CLient", + }, + Field: integration.SettingTypeText{ + Text: integration.SettingTypeTextDefinition{ + Value: "8080", + }, + }, + } + + websocketport := integration.SetupDataSchemaSettings{ + Id: "websocketport", + Label: integration.LanguageText{ + En: "Websocket Port used by your deCONZ CLient", + }, + Field: integration.SettingTypeText{ + Text: integration.SettingTypeTextDefinition{ + Value: "8081", + }, + }, + } + + metadata := integration.DriverMetadata{ + DriverId: "deCONZ", + Developer: integration.Developer{ + Name: "Sebastian Plattner", + }, + Name: integration.LanguageText{ + En: "DeCONZ", + }, + Version: "0.0.1", + SetupDataSchema: integration.SetupDataSchema{ + Title: integration.LanguageText{ + En: "Configuration", + De: "Konfiguration", + }, + Settings: []integration.SetupDataSchemaSettings{ipaddr, port, websocketport}, + }, + Icon: "", + } + + client.IntegrationDriver.SetMetadata(&metadata) + + // set the client specific functions + client.initFunc = client.initDeconzClient + client.setupFunc = client.deconzHandleSetup + client.clientLoopFunc = client.deconzClientLoop + client.setDriverUserDataFunc = client.handleSetDriverUserData + + return &client +} + +func (c *DeconzClient) handleSetDriverUserData(user_data map[string]string, confirm bool) { + + log.Debug("Deconz handle set driver user data") + + // confirm seens to be set to false always, maybe just the presence of the field tells be, + // confirmation was sent? + if len(user_data) == 0 { + // Get a new Denon API Key + + ipaddr := c.IntegrationDriver.SetupData["ipaddr"] + port, _ := strconv.Atoi(c.IntegrationDriver.SetupData["port"]) + websocketport, _ := strconv.Atoi(c.IntegrationDriver.SetupData["websocketport"]) + + deconz := deconz.NewDeconz(ipaddr, port, websocketport, "") + apikey, err := deconz.GetNewAPIKey(c.IntegrationDriver.DriverId) + + if err != nil { + log.WithError(err).Debug("Failed to get new api Key") + c.IntegrationDriver.SetDriverSetupState(integration.StopEvent, integration.ErrorDeviceState, integration.AuthErrorError, nil) + return + } + + c.IntegrationDriver.SetupData["apikey"] = apikey + c.IntegrationDriver.PersistSetupData() + + c.IntegrationDriver.SetDriverSetupState(integration.StopEvent, integration.OkState, "", nil) + + } +} + +func (c *DeconzClient) initDeconzClient() { + +} + +func (c *DeconzClient) deconzHandleSetup() { + //event_type: SETUP with state: SETUP is a progress event to keep the process running, + // If the setup process takes more than a few seconds, + // the integration should send driver_setup_change events with state: SETUP to the Remote Two + // to show a setup progress to the user and prevent an inactivity timeout. + //c.IntegrationDriver.SetDriverSetupState(integration.SetupEvent, integration.SetupState, "", nil) + time.Sleep(1 * time.Second) + + var userAction = integration.RequireUserAction{ + Confirmation: integration.ConfirmationPage{ + Title: integration.LanguageText{ + En: "Gateway configuration", + }, + Message1: integration.LanguageText{ + En: "Please unlock your DeCONZ Gateway to create a new API Key", + }, + }, + } + + // Start the setup with some require user data + c.IntegrationDriver.SetDriverSetupState(integration.SetupEvent, integration.WaitUserActionState, "", &userAction) + + // // Finish the setup + //c.IntegrationDriver.SetDriverSetupState(integration.StopEvent, integration.OkState, "", nil) + +} + +func (c *DeconzClient) setupDeconz() { + + if c.IntegrationDriver.SetupData["apikey"] != "" { + + ipaddr := c.IntegrationDriver.SetupData["ipaddr"] + port, _ := strconv.Atoi(c.IntegrationDriver.SetupData["port"]) + websocketport, _ := strconv.Atoi(c.IntegrationDriver.SetupData["websocketport"]) + + log.WithFields(log.Fields{ + "ipaddr": ipaddr, + "port": port, + "websocketport": websocketport, + }).Debug("Create DeCONZ Client") + + deconz := deconz.NewDeconz(ipaddr, port, websocketport, c.IntegrationDriver.SetupData["apikey"]) + c.deconz = deconz + } + +} + +func (c *DeconzClient) configureDeconz() { + + log.Debug("Configure DeCONZ") + + c.deconz.SetDeviceDiscoveredHandler(c.handleNewDeviceDiscovered) + c.deconz.SetDeviceRemoveHandler(c.handleRemoveDevice) + + // TODO, enable groups as setup_data + go c.deconz.StartDiscovery(true) + +} + +func (c *DeconzClient) handleNewDeviceDiscovered(device *deconz.DeconzDevice) { + log.WithFields(log.Fields{ + "id": device.GetID(), + "type": device.Type, + "name": device.GetName(), + }).Debug("New Deconz Device discovered") + + switch device.Type { + case deconz.SensorDeconzDeviceType: + //sensor := entities.NewSensorEntity(fmt.Sprintf("light%d", device.GetID()), entities.LanguageText{En: device.GetName()}, "") + + //c.IntegrationDriver.AddEntity(sensor) + + case deconz.LightDeconzDeviceType: + light := entities.NewLightEntity(fmt.Sprintf("light%d", device.GetID()), entities.LanguageText{En: device.GetName()}, "") + + // All correct Attributes + light.AddFeature(entities.OnOffLightEntityFeatures) + light.AddFeature(entities.ToggleLightEntityFeatures) + light.AddFeature(entities.DimLightEntityFeatures) + + if device.Light.HasColor { + switch device.Light.State.ColorMode { + case "ct": + light.AddFeature(entities.ColorTemperatureLightEntityFeatures) + case "hs": + light.AddFeature(entities.ColorLightEntityFeatures) + } + } + + // Set initial attribute + if light.HasAttribute(entities.StateLightEntityAttribute) { + if *device.Light.State.On { + light.Attributes[string(entities.StateLightEntityAttribute)] = entities.OnLightEntityState + } else { + light.Attributes[string(entities.StateLightEntityAttribute)] = entities.OffLightEntityState + } + } + + if light.HasAttribute(entities.BrightnessLightEntityAttribute) { + light.Attributes[string(entities.BrightnessLightEntityAttribute)] = device.Light.State.Bri + + } + + // Add commands + light.AddCommand(entities.OnLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + + if err := device.TurnOn(); err != nil { + return 404 + } + return 200 + }) + + light.AddCommand(entities.OffLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + + if err := device.TurnOff(); err != nil { + return 404 + } + return 200 + }) + + light.AddCommand(entities.ToggleLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + if device.IsOn() { + device.TurnOff() + } else { + device.TurnOff() + } + return 200 + }) + + // Set the Handle State Change function + device.SetHandleChangeStateFunc(func(state *deconz.DeconzState) { + log.WithFields(log.Fields{ + "ID": device.GetID(), + "Type": device.Type, + }).Debug("Handle State Change") + + attributes := make(map[string]interface{}) + + if light.HasAttribute(entities.StateLightEntityAttribute) { + if *state.On { + attributes[string(entities.StateLightEntityAttribute)] = entities.OnLightEntityState + } else { + attributes[string(entities.StateLightEntityAttribute)] = entities.OffLightEntityState + } + } + + if light.HasAttribute(entities.BrightnessLightEntityAttribute) { + attributes[string(entities.BrightnessLightEntityAttribute)] = state.Bri + + } + + light.SetAttributes(attributes) + + }) + + c.IntegrationDriver.AddEntity(light) + + case deconz.GroupDeconzDeviceType: + + group := entities.NewLightEntity(fmt.Sprintf("group%d", device.GetID()), entities.LanguageText{En: device.GetName()}, "") + // Group only allows for on/off -> basic switch, no dimming + group.AddFeature(entities.OnOffLightEntityFeatures) + group.AddFeature(entities.ToggleLightEntityFeatures) + + // Set initial attribute + // if group.HasAttribute(entities.StateLightEntityAttribute) { + // if *&device.Group.State.AnyOn { + // light.Attributes[string(entities.StateLightEntityAttribute)] = entities.OnLightEntityState + // } else { + // light.Attributes[string(entities.StateLightEntityAttribute)] = entities.OffLightEntityState + // } + // } + + // Commands + + group.AddCommand(entities.OnLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + + if err := device.TurnOn(); err != nil { + return 404 + } + return 200 + }) + + group.AddCommand(entities.OffLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + + if err := device.TurnOff(); err != nil { + return 404 + } + return 200 + }) + + group.AddCommand(entities.ToggleLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + if device.IsOn() { + device.TurnOff() + } else { + device.TurnOff() + } + return 200 + }) + + device.SetHandleChangeStateFunc(func(state *deconz.DeconzState) { + log.WithFields(log.Fields{ + "ID": device.GetID(), + "Type": device.Type, + }).Debug("Handle State Change") + + attributes := make(map[string]interface{}) + + if group.HasAttribute(entities.StateLightEntityAttribute) { + if *&state.AnyOn { + attributes[string(entities.StateLightEntityAttribute)] = entities.OnLightEntityState + } else { + attributes[string(entities.StateLightEntityAttribute)] = entities.OffLightEntityState + } + } + + group.SetAttributes(attributes) + + }) + + c.IntegrationDriver.AddEntity(group) + } + +} + +func (c *DeconzClient) handleRemoveDevice(device *deconz.DeconzDevice) { + log.WithFields(log.Fields{ + "ID": device.GetID(), + "Name": device.GetName(), + "Type": device.Type, + }).Debug("Deconz Device not available anymore") + + switch device.Type { + case deconz.SensorDeconzDeviceType: + + case deconz.LightDeconzDeviceType: + light := entities.NewLightEntity(fmt.Sprintf("light%d", device.GetID()), entities.LanguageText{En: device.GetName()}, "") + c.IntegrationDriver.RemoveEntity(light) + + case deconz.GroupDeconzDeviceType: + group := entities.NewLightEntity(fmt.Sprintf("group%d", device.GetID()), entities.LanguageText{En: device.GetName()}, "") + c.IntegrationDriver.RemoveEntity(group) + } + +} + +// Callen on RT connect +func (c *DeconzClient) deconzClientLoop() { + + defer func() { + c.deconz.Stop() + c.setDeviceState(integration.DisconnectedDeviceState) + }() + + if c.deconz == nil { + c.setupDeconz() + } else { + return + } + + if c.deconz != nil { + c.configureDeconz() + + go c.deconz.StartandListenLoop() + + } else { + return + } + + // Handle connection to device this integration shall control + // Set Device state to connected when connection is established + c.setDeviceState(integration.ConnectedDeviceState) + + // Run Client Loop to handle entity changes from device + for { + select { + case msg := <-c.messages: + + switch msg { + case "disconnect": + return + } + + } + } + +} diff --git a/pkg/client/denonavrclient.go b/pkg/client/denonavrclient.go index 923e3e1..f070bcb 100644 --- a/pkg/client/denonavrclient.go +++ b/pkg/client/denonavrclient.go @@ -95,11 +95,6 @@ func (c *DenonAVRClient) initDenonAVRClient() { c.moniAutoButton = entities.NewButtonEntity("moniauto", entities.LanguageText{En: "Monitor Out Auto"}, "") c.IntegrationDriver.AddEntity(c.moniAutoButton) - // Pass function to the integration driver that is called when the remote want to setup the driver - c.IntegrationDriver.SetHandleSetupFunction(c.HandleSetup) - // Pass function to the integration driver that is called when the remote want to setup the driver - c.IntegrationDriver.SetHandleConnectionFunction(c.HandleConnection) - c.IntegrationDriver.SetHandleSetDriverUserDataFunction(c.HandleSetDriverUserDataFunction) } func (c *DenonAVRClient) denonHandleSetup() { diff --git a/pkg/cmd/deconz/deconz.go b/pkg/cmd/deconz/deconz.go new file mode 100644 index 0000000..a1bf2bc --- /dev/null +++ b/pkg/cmd/deconz/deconz.go @@ -0,0 +1,64 @@ +package deconz + +import ( + "os" + + log "github.com/sirupsen/logrus" + + "github.com/spf13/cobra" + "github.com/splattner/goucrt/pkg/client" + "github.com/splattner/goucrt/pkg/cmd" + "github.com/splattner/goucrt/pkg/integration" +) + +func NewCommand(rootCmd *cobra.Command) *cobra.Command { + + var command = &cobra.Command{ + Use: "deconz", + Short: "Deconz", + Long: "Deconz Integration for a Unfolded Circle Remote Two", + Run: func(c *cobra.Command, args []string) { + + log.SetOutput(os.Stdout) + + debug, _ := rootCmd.Flags().GetBool("debug") + if debug { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(log.InfoLevel) + } + + var config = make(integration.Config) + + listenPort, _ := rootCmd.Flags().GetInt("listenPort") + enableMDNS, _ := rootCmd.Flags().GetBool("mdns") + enableRegistration, _ := rootCmd.Flags().GetBool("registration") + registrationUsername, _ := rootCmd.Flags().GetString("registrationUsername") + registrationPin, _ := rootCmd.Flags().GetString("registrationPin") + websocketPath, _ := rootCmd.Flags().GetString("websocketPath") + remoteTwoIP, _ := rootCmd.Flags().GetString("remoteTwoIP") + remoteTwoPort, _ := rootCmd.Flags().GetInt("remoteTwoPort") + + config["listenport"] = listenPort + config["enableMDNS"] = enableMDNS + config["enableRegistration"] = enableRegistration + config["registrationUsername"] = registrationUsername + config["registrationPin"] = registrationPin + config["remoteTwoIP"] = remoteTwoIP + config["remoteTwoPort"] = remoteTwoPort + config["websocketPath"] = websocketPath + + i, err := integration.NewIntegration(config) + cmd.CheckError(err) + + myclient := client.NewDeconzClient(i) + + myclient.InitClient() + + cmd.CheckError(i.Run()) + + }, + } + + return command +} diff --git a/pkg/cmd/ucrt/ucrt.go b/pkg/cmd/ucrt/ucrt.go index e2a78e6..439d93f 100644 --- a/pkg/cmd/ucrt/ucrt.go +++ b/pkg/cmd/ucrt/ucrt.go @@ -2,6 +2,7 @@ package ucrt import ( "github.com/spf13/cobra" + "github.com/splattner/goucrt/pkg/cmd/deconz" "github.com/splattner/goucrt/pkg/cmd/denonavr" ) @@ -27,6 +28,7 @@ func NewCommand(name string) *cobra.Command { rootCmd.AddCommand( denonavr.NewCommand(rootCmd), + deconz.NewCommand(rootCmd), ) return rootCmd diff --git a/pkg/deconz/apikey.go b/pkg/deconz/apikey.go new file mode 100644 index 0000000..4749ed0 --- /dev/null +++ b/pkg/deconz/apikey.go @@ -0,0 +1,78 @@ +package deconz + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + log "github.com/sirupsen/logrus" +) + +type DeconzAPIKeyReqResponse struct { + Success DeconzAPIKeyReqResponseData `json:"success"` +} + +type DeconzAPIKeyReqResponseData struct { + Username string `json:"username"` +} + +// Get a new API key from DeconZ +func (d *Deconz) GetNewAPIKey(devicetype string) (string, error) { + log.WithFields(log.Fields{ + "host": d.host, + "port": d.port, + }).Info("Get a new API Key from DeCONZ") + + url := "http://" + d.host + ":" + fmt.Sprint(d.port) + "/api" + + jsonBody := []byte(`{"devicetype": "` + devicetype + `"}`) + bodyReader := bytes.NewReader(jsonBody) + + req, err := http.NewRequest("POST", url, bodyReader) + if err != nil { + log.WithError(err).Fatal("impossible to build request") + return "", err + } + + // send the request + client := http.Client{Timeout: 10 * time.Second} + res, err := client.Do(req) + if err != nil { + log.WithError(err).Fatal("Failed to send the request") + return "", err + } + + defer res.Body.Close() + + statusCode := res.StatusCode + resBody, err := io.ReadAll(res.Body) + if err != nil { + log.Fatalf("impossible to read all body of response: %s", err) + return "", err + } + + log.WithFields(log.Fields{ + "Status Code": statusCode, + "Response": string(resBody)}).Debug("Get DeconZ API Key") + + switch statusCode { + case http.StatusForbidden: + return "", fmt.Errorf("Make sure your Gateway is unlocked by pressing the link button") + + case http.StatusOK: + var response []DeconzAPIKeyReqResponse + if err := json.Unmarshal(resBody, &response); err != nil { + return "", err + } + + for _, item := range response { + d.apikey = item.Success.Username + } + + } + + return d.apikey, nil +} diff --git a/pkg/deconz/deconz.go b/pkg/deconz/deconz.go new file mode 100644 index 0000000..e9915ef --- /dev/null +++ b/pkg/deconz/deconz.go @@ -0,0 +1,163 @@ +package deconz + +import ( + deconzgroup "github.com/jurgen-kluft/go-conbee/groups" + deconzlight "github.com/jurgen-kluft/go-conbee/lights" + deconzsensor "github.com/jurgen-kluft/go-conbee/sensors" + log "github.com/sirupsen/logrus" +) + +type Deconz struct { + host string + port int + websocketport int + apikey string + + // Array with all lights, groups, sensors + allDeconzDevices []*DeconzDevice + + controlChannel chan string + + handleDeviceDiscoveredFunc func(*DeconzDevice) + handleDeviceRemoveFunc func(*DeconzDevice) +} + +// Create a new DeCONZ client +func NewDeconz(host string, port int, websocketport int, apikey string) *Deconz { + + deconz := Deconz{} + deconz.host = host + deconz.port = port + deconz.websocketport = websocketport + deconz.apikey = apikey + + deconz.controlChannel = make(chan string) + + return &deconz +} + +// Set the function that get called when a new Deconz Device is discovered +func (d *Deconz) SetDeviceDiscoveredHandler(f func(*DeconzDevice)) { + d.handleDeviceDiscoveredFunc = f +} + +// Set the function that get called when a Deconz Device is no longer available anymore +func (d *Deconz) SetDeviceRemoveHandler(f func(*DeconzDevice)) { + d.handleDeviceRemoveFunc = f +} + +// Add a new device if not already available +// Call handleDeviceDiscovered function +func (d *Deconz) addDevice(newDevice *DeconzDevice) { + + for _, d := range d.allDeconzDevices { + if d.GetID() == newDevice.GetID() && d.Type == newDevice.Type { + log.WithFields(log.Fields{ + "ID": newDevice.GetID, + "Type": newDevice.Type, + }).Debug("Device already available") + return + } + } + log.WithFields(log.Fields{ + "ID": newDevice.GetID(), + "Type": newDevice.Type, + }).Debug("Add Device and call handleDeviceDiscovered Func") + d.allDeconzDevices = append(d.allDeconzDevices, newDevice) + + if d.handleDeviceDiscoveredFunc != nil { + d.handleDeviceDiscoveredFunc(newDevice) + } + +} + +// Check all devices existing if they were still discovered +// Otherwise remove it +// Call handleDeviceRemoveFunc +func (d *Deconz) removeDevice(allDevices interface{}) { + + var toRemove []*DeconzDevice + + switch allDevices := allDevices.(type) { + case []deconzsensor.Sensor: + + // Loop trought the existing devices + for _, dd := range d.allDeconzDevices { + if dd.Type == SensorDeconzDeviceType { + // Check if in discovered device + remove := true + for _, device := range allDevices { + + // Compare by id + if dd.GetID() == device.ID { + //Still available, so don't remove + remove = false + + break + } + } + + if remove { + toRemove = append(toRemove, dd) + } + } + } + + case []deconzlight.Light: + // Loop trought the existing devices + for _, dd := range d.allDeconzDevices { + if dd.Type == LightDeconzDeviceType { + // Check if in discovered device + remove := true + for _, device := range allDevices { + + // Compare by id + if dd.GetID() == device.ID { + // Still available, so don't remove + remove = false + + break + } + } + if remove { + toRemove = append(toRemove, dd) + } + } + } + + case []deconzgroup.Group: + // Loop trought the existing devices + for _, dd := range d.allDeconzDevices { + if dd.Type == GroupDeconzDeviceType { + // Check if in discovered device + remove := true + for _, device := range allDevices { + + // Compare by id + if dd.GetID() == device.ID { + // Still available, so don't remove + remove = false + break + } + + } + if remove { + toRemove = append(toRemove, dd) + } + + } + } + } + + // Finaly remote thos who are not needed anymore and call device removed handler + for ix, device := range toRemove { + d.allDeconzDevices[ix] = d.allDeconzDevices[len(d.allDeconzDevices)-1] // Copy last element to index i. + d.allDeconzDevices[len(d.allDeconzDevices)-1] = nil // Erase last element (write zero value). + d.allDeconzDevices = d.allDeconzDevices[:len(d.allDeconzDevices)-1] // Truncate slice. + + if d.handleDeviceRemoveFunc != nil { + d.handleDeviceRemoveFunc(device) + } + } + +} diff --git a/pkg/deconz/device.go b/pkg/deconz/device.go new file mode 100644 index 0000000..d3161e2 --- /dev/null +++ b/pkg/deconz/device.go @@ -0,0 +1,231 @@ +package deconz + +import ( + "fmt" + "math" + + deconzgroup "github.com/jurgen-kluft/go-conbee/groups" + deconzlight "github.com/jurgen-kluft/go-conbee/lights" + deconzsensor "github.com/jurgen-kluft/go-conbee/sensors" +) + +type DeconzDeviceType string + +const ( + LightDeconzDeviceType DeconzDeviceType = "light" + GroupDeconzDeviceType = "group" + SensorDeconzDeviceType = "sensor" +) + +type DeconzDevice struct { + deconz *Deconz + + Type DeconzDeviceType + + Light deconzlight.Light + Group deconzgroup.Group + Sensor deconzsensor.Sensor + + // For when there are multiple buttons on one sensor + // sensorButtonId is the identifier to get the correct device + sensorButtonId int + + handleStateChangeFunc func(state *DeconzState) +} + +// Call Tyoe specific functions +// Add Device to DeCONZ Client +func (d *DeconzDevice) NewDeconzDevice(deconz *Deconz) { + + d.deconz = deconz + + switch d.Type { + case LightDeconzDeviceType: + d.newDeconzLightDevice() + case GroupDeconzDeviceType: + d.newDeconzGroupDevice() + case SensorDeconzDeviceType: + d.newDeconzSensorDevice() + } + + d.deconz.addDevice(d) + +} + +// Set the function that is called when a Stage change event is receiverd from DeCONZ Websocket +func (d *DeconzDevice) SetHandleChangeStateFunc(f func(state *DeconzState)) { + d.handleStateChangeFunc = f +} + +// Call the State Change Handler for this device +func (d *DeconzDevice) stateChangeHandler(state *DeconzState) { + if d.handleStateChangeFunc != nil { + d.handleStateChangeFunc(state) + } + +} + +// Return the ID of the Device based on its type +func (d *DeconzDevice) GetID() int { + + switch d.Type { + case LightDeconzDeviceType: + return d.Light.ID + case GroupDeconzDeviceType: + return d.Group.ID + case SensorDeconzDeviceType: + return d.Sensor.ID + } + + return 0 +} + +// Return the Name of the Device based on its type +func (d *DeconzDevice) GetName() string { + + switch d.Type { + case LightDeconzDeviceType: + return d.Light.Name + case GroupDeconzDeviceType: + return d.Group.Name + case SensorDeconzDeviceType: + return d.Sensor.Name + } + + return "" +} + +// Apply update from deconz device +func (d *DeconzDevice) SetValue(value float32, channelName string) error { + + switch channelName { + + case "basic_switch", "brightness": + brightness := float32(math.Round(float64(value))) + return d.SetBrightness(brightness) + case "hue": + return d.SetHue(value) + case "saturation": + return d.SetSaturation(value) + case "colortemp": + return d.SetColorTemp(value) + } + + return fmt.Errorf("Channel Name not found") +} + +func (d *DeconzDevice) TurnOn() error { + + switch d.Type { + case LightDeconzDeviceType: + d.Light.State.SetOn(true) + case GroupDeconzDeviceType: + d.Group.Action.SetOn(true) + } + + return d.setState() +} + +func (d *DeconzDevice) TurnOff() error { + + switch d.Type { + case LightDeconzDeviceType: + d.Light.State.SetOn(false) + case GroupDeconzDeviceType: + d.Group.Action.SetOn(false) + } + + return d.setState() +} + +func (d *DeconzDevice) IsOn() bool { + switch d.Type { + case LightDeconzDeviceType: + return *d.Light.State.On + case GroupDeconzDeviceType: + return *d.Group.Action.On + } + + return false +} + +func (d *DeconzDevice) SetBrightness(brightness float32) error { + + switch d.Type { + case LightDeconzDeviceType: + if brightness == 0 { + d.Light.State.SetOn(false) + } else { + d.Light.State.SetOn(true) + } + + bri_converted := uint8(math.Round(float64(brightness) / 100 * 255)) + d.Light.State.Bri = &bri_converted + case GroupDeconzDeviceType: + if brightness == 0 { + d.Group.Action.SetOn(false) + } else { + d.Group.Action.SetOn(true) + } + + bri_converted := uint8(math.Round(float64(brightness) / 100 * 255)) + d.Group.Action.Bri = &bri_converted + } + + return d.setState() +} + +func (d *DeconzDevice) SetColorTemp(ct float32) error { + + converted := uint16(ct) + + switch d.Type { + case LightDeconzDeviceType: + d.Light.State.CT = &converted + case GroupDeconzDeviceType: + d.Group.Action.CT = &converted + } + + return d.setState() +} + +func (d *DeconzDevice) SetHue(hue float32) error { + + converted := uint16(hue) + + switch d.Type { + case LightDeconzDeviceType: + d.Light.State.Hue = &converted + case GroupDeconzDeviceType: + d.Group.Action.Hue = &converted + } + + return d.setState() +} + +func (d *DeconzDevice) SetSaturation(saturation float32) error { + + converted := uint8(saturation) + + switch d.Type { + case LightDeconzDeviceType: + d.Light.State.Sat = &converted + case GroupDeconzDeviceType: + d.Group.Action.Sat = &converted + } + + return d.setState() +} + +func (d *DeconzDevice) setState() error { + + switch d.Type { + case LightDeconzDeviceType: + return d.setLightState() + case GroupDeconzDeviceType: + return d.setGroupState() + } + + return fmt.Errorf("Device Type not found") + +} diff --git a/pkg/deconz/discovery.go b/pkg/deconz/discovery.go new file mode 100644 index 0000000..a3154d6 --- /dev/null +++ b/pkg/deconz/discovery.go @@ -0,0 +1,130 @@ +package deconz + +import ( + "fmt" + + deconzgroup "github.com/jurgen-kluft/go-conbee/groups" + deconzlight "github.com/jurgen-kluft/go-conbee/lights" + deconzsensor "github.com/jurgen-kluft/go-conbee/sensors" + log "github.com/sirupsen/logrus" +) + +func (d *Deconz) StartDiscovery(enableGroups bool) { + + log.WithField("DeCONZ Host", d.host).Info("Starting Deconz device discovery") + + if d.apikey == "" { + log.Fatal("API Key is not set, you first need to aquire a API Key") + return + } + + deconzHost := d.host + ":" + fmt.Sprint(d.port) + + // Lights + dl := deconzlight.New(deconzHost, d.apikey) + allLights, err := dl.GetAllLights() + if err != nil { + log.WithError(err).Debug("Error getting all Lights from Deconz") + } + log.WithField("lights", allLights).Trace("Deconz Discovery") + for _, light := range allLights { + d.lightsDiscovery(light) + } + // Remote those devices that were not discovered anymore + d.removeDevice(allLights) + + // Groups + if enableGroups { + dg := deconzgroup.New(deconzHost, d.apikey) + allGroups, err := dg.GetAllGroups() + if err != nil { + log.WithError(err).Debug("Error getting all Groups from Deconz") + } + log.WithField("groups", allGroups).Trace("Deconz Discovery") + for _, group := range allGroups { + d.groupsDiscovery(group) + } + // Remote those devices that were not discovered anymore + d.removeDevice(allGroups) + } + + // Sensors + ds := deconzsensor.New(deconzHost, d.apikey) + allSensors, err := ds.GetAllSensors() + if err != nil { + log.WithError(err).Debug("Error getting all Sensors from Deconz") + } + log.WithField("sonsors", allSensors).Trace("Deconz Discovery") + for _, sensor := range allSensors { + d.sensorDiscovery(sensor) + } + // Remote those devices that were not discovered anymore + d.removeDevice(allSensors) + + log.Info("Deconz, Device Discovery finished") +} + +func (d *Deconz) groupsDiscovery(group deconzgroup.Group) { + + log.WithFields(log.Fields{ + "Name": group.Name, + "ID": group.ID, + }).Debug("Found new Group") + + if len(group.Lights) > 0 { + + deconzDevice := new(DeconzDevice) + + deconzDevice.Type = GroupDeconzDeviceType + deconzDevice.Group = group + + log.WithField("Name", group.Name).Debug("Deconz, Group discovered") + + deconzDevice.NewDeconzDevice(d) + } +} + +func (d *Deconz) lightsDiscovery(light deconzlight.Light) { + log.WithFields(log.Fields{ + "Name": light.Name, + "Type": light.Type, + "UniqueID": light.UniqueID, + "ID": light.ID, + }).Debug("Found new Light") + + if light.Type != "Configuration tool" { // filter this out + deconzDevice := new(DeconzDevice) + + deconzDevice.Type = LightDeconzDeviceType + deconzDevice.Light = light + + log.WithField("Name", light.Name).Debug("Deconz, Lights discovered") + + deconzDevice.NewDeconzDevice(d) + + } + +} + +func (d *Deconz) sensorDiscovery(sensor deconzsensor.Sensor) { + + log.WithFields(log.Fields{ + "Name": sensor.Name, + "Type": sensor.Type, + "UniqueID": sensor.UniqueID, + "ID": sensor.ID, + }).Debug("Found new Sensor") + + // See https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/sensors/#supported-sensor-types-and-states + switch sensor.Type { + case "ZHAOpenClose", "ZHATemperature", "ZHAHumidity", "ZHAPressure": + deconzDevice := new(DeconzDevice) + deconzDevice.Type = SensorDeconzDeviceType + deconzDevice.Sensor = sensor + + log.WithField("Name", sensor.Name).Debug("Deconz, Sensor discovered") + + deconzDevice.NewDeconzDevice(d) + } + +} diff --git a/pkg/deconz/group.go b/pkg/deconz/group.go new file mode 100644 index 0000000..e207c1b --- /dev/null +++ b/pkg/deconz/group.go @@ -0,0 +1,30 @@ +package deconz + +import ( + "fmt" + "strings" + + deconzgroup "github.com/jurgen-kluft/go-conbee/groups" + log "github.com/sirupsen/logrus" +) + +func (d *DeconzDevice) newDeconzGroupDevice() { + +} + +func (d *DeconzDevice) setGroupState() error { + + state := strings.Replace(d.Group.Action.String(), "\n", ",", -1) + state = strings.Replace(state, " ", "", -1) + + log.Infof("Deconz, call SetGroupState with state (%s) for Light with id %d\n", state, d.Group.ID) + + conbeehost := fmt.Sprintf("%s:%d", d.deconz.host, d.deconz.port) + ll := deconzgroup.New(conbeehost, d.deconz.apikey) + _, err := ll.SetGroupState(d.Light.ID, d.Group.Action) + if err != nil { + log.Debugln("Deconz, SetGroupState Error", err) + return err + } + return nil +} diff --git a/pkg/deconz/light.go b/pkg/deconz/light.go new file mode 100644 index 0000000..a8e25f6 --- /dev/null +++ b/pkg/deconz/light.go @@ -0,0 +1,31 @@ +package deconz + +import ( + "fmt" + "strings" + + deconzlight "github.com/jurgen-kluft/go-conbee/lights" + log "github.com/sirupsen/logrus" +) + +func (d *DeconzDevice) newDeconzLightDevice() { + +} + +func (d *DeconzDevice) setLightState() error { + + state := strings.Replace(d.Light.State.String(), "\n", ",", -1) + state = strings.Replace(state, " ", "", -1) + + log.Infof("Deconz, call SetLightState with state (%s) for Light with id %d\n", state, d.Light.ID) + + conbeehost := fmt.Sprintf("%s:%d", d.deconz.host, d.deconz.port) + ll := deconzlight.New(conbeehost, d.deconz.apikey) + _, err := ll.SetLightState(d.Light.ID, &d.Light.State) + if err != nil { + log.Debugln("Deconz, SetLightState Error", err) + return err + } + + return nil +} diff --git a/pkg/deconz/sensor.go b/pkg/deconz/sensor.go new file mode 100644 index 0000000..0c905a6 --- /dev/null +++ b/pkg/deconz/sensor.go @@ -0,0 +1,36 @@ +package deconz + +import ( + "fmt" +) + +type ButtonEvent int + +// http://developer.digitalstrom.org/Architecture/ds-basics.pdf +const ( + Hold ButtonEvent = iota + 1 + ShortRelease + LongRelease + DoublePress + TreeplePress +) + +const ( + // milisecs + SingleTip int = 150 + SingleClick int = 50 +) + +func (d *DeconzDevice) newDeconzSensorDevice() { + +} + +func (e *DeconzDevice) getUniqueId() string { + uniqueID := fmt.Sprintf("%s-%d", e.Sensor.UniqueID, e.sensorButtonId) + return uniqueID +} + +func (e *DeconzDevice) getName() string { + name := fmt.Sprintf("%s Button %d", e.Sensor.Name, e.sensorButtonId+1) + return name +} diff --git a/pkg/deconz/types.go b/pkg/deconz/types.go new file mode 100644 index 0000000..79b1c67 --- /dev/null +++ b/pkg/deconz/types.go @@ -0,0 +1,55 @@ +package deconz + +type DeconzWebSocketMessage struct { + Type string `json:"t,omitempty"` + Event string `json:"e,omitempty"` + Resource string `json:"r,omitempty"` + ID string `json:"id,omitempty"` + UniqueID string `json:"uniqueid,omitempty"` + GroupID string `json:"gid,omitempty"` + SceneID string `json:"scid,omitempty"` + Name string `json:"name,omitempty"` + Attributes DeconzLightAttribute `json:"attr,omitempty"` + State DeconzState `json:"state,omitempty"` +} + +type DeconzLightAttribute struct { + Id string `json:"id,omitempty"` + LastAnnounced string `json:"lastannounced,omitempty"` + LastSeen string `json:"lastseen,omitempty"` + ManufacturerName string `json:"manufacturername,omitempty"` + ModelId string `json:"modelid,omitempty"` + Name string `json:"name,omitempty"` + SWVersion string `json:"swversion,omitempty"` + Type string `json:"type,omitempty"` + UniqueID string `json:"uniqueid,omitempty"` + ColorCapabilities int `json:"colorcapabilities,omitempty"` + Ctmax int `json:"ctmax,omitempty"` + Ctmin int `json:"ctmin,omitempty"` +} + +type DeconzState struct { + + // Light & Group + On *bool `json:"on,omitempty"` // + Hue *uint16 `json:"hue,omitempty"` // + Effect string `json:"effect,omitempty"` // + Bri *uint8 `json:"bri,omitempty"` // min = 1, max = 254 + Sat *uint8 `json:"sat,omitempty"` // + CT *uint16 `json:"ct,omitempty"` // min = 154, max = 500 + XY []float32 `json:"xy,omitempty"` + Alert string `json:"alert,omitempty"` + + // Light + Reachable *bool `json:"reachable,omitempty"` + ColorMode string `json:"colormode,omitempty"` + ColorLoopSpeed *uint8 `json:"colorloopspeed,omitempty"` + TransitionTime *uint16 `json:"transitiontime,omitempty"` + + // Group + AllOn bool `json:"all_on,omitempty"` + AnyOn bool `json:"any_on,omitempty"` + + // Sensor + ButtonEvent int `json:"buttonevent,omitempty"` +} diff --git a/pkg/deconz/websocket.go b/pkg/deconz/websocket.go new file mode 100644 index 0000000..ac3c27d --- /dev/null +++ b/pkg/deconz/websocket.go @@ -0,0 +1,178 @@ +package deconz + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" +) + +const ( + + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 512 +) + +// Stop the listen Loop +func (d *Deconz) Stop() { + + d.controlChannel <- "stop" + +} + +// Connect to DeCONZ Websocket and start listening for events +func (d *Deconz) StartandListenLoop() { + + log.Info("Deconz, Starting Deconz Websocket Loop") + + ticker := time.NewTicker(pingPeriod) + + socketUrl := fmt.Sprintf("ws://%s:%d", d.host, d.websocketport) + log.WithField("SocketURL", socketUrl).Debug("Deconz,Trying to connect to Deconz Websocket") + ws, _, err := websocket.DefaultDialer.Dial(socketUrl, nil) + if err != nil { + log.Fatal("Deconz, Error connecting to Websocket Server:", err) + } + log.Debugln("Deconz, Connected to Deconz websocket") + + defer func() { + log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Closing Websocket") + ws.Close() + ticker.Stop() + }() + + go d.websocketReceiveHandler(ws) + + // Our main loop for the client + // We send our relevant packets here + log.Debugln("Deconz, Starting Deconz Websocket client main loop") + for { + select { + case <-d.controlChannel: + log.Debug("Closing write loop as read loop closed") + return + + case <-ticker.C: + ws.SetWriteDeadline(time.Now().Add(writeWait)) + log.WithField("RemoteAddr", ws.RemoteAddr().String()).Debug("Deconz, Send Ping Message") + if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil { + log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Could not send Ping message") + return + } + // case <-time.After(time.Duration(1) * time.Millisecond * 1000): + // // Send an echo packet every second + // err := conn.WriteMessage(websocket.TextMessage, []byte("Hello from vdcd-brige!")) + // if err != nil { + // log.WithError(err).Debug("Deconz, Error during writing to websocket") + // return + // } + + } + } +} + +// Read from Websocket and process events +func (d *Deconz) websocketReceiveHandler(ws *websocket.Conn) { + + log.Info("Deconz, Starting Deconz Websocket receive handler") + + ws.SetReadLimit(maxMessageSize) + ws.SetReadDeadline(time.Now().Add(pongWait)) + ws.SetPongHandler(func(string) error { + ws.SetReadDeadline(time.Now().Add(pongWait)) + return nil + }) + + defer func() { + log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Closing Websocket, not able to read message anymore") + ws.Close() + // Notify Write looĆ¼ + d.controlChannel <- ws.RemoteAddr().String() + }() + + for { + _, msg, err := ws.ReadMessage() + if err != nil { + log.WithError(err).Debug("Deconz, Error in Deconz Websocket Message receive") + return + } + + log.WithField("message", string(msg)).Trace("Deconz, Received Deconz Websocket Message") + + var message DeconzWebSocketMessage + err = json.Unmarshal(msg, &message) + + if err != nil { + log.WithError(err).Debug("Unmarshal to DeconzWebSocketMessage failed") + return + } + + // Handling light Resources + if message.Type == "event" && message.Resource == "lights" && message.Event == "changed" { + if message.State.On != nil || + message.State.Hue != nil || + message.State.Effect != "" || + message.State.Bri != nil || + message.State.Sat != nil || + message.State.CT != nil || + message.State.Reachable != nil || + message.State.ColorMode != "" || + message.State.ColorLoopSpeed != nil { + + for _, l := range d.allDeconzDevices { + if l.Type == LightDeconzDeviceType { + if fmt.Sprint(l.Light.ID) == message.ID { + log.WithField("name", l.Light.Name).Debug("Deconz Websocket changed event for light") + l.stateChangeHandler(&message.State) + break + } + + } + + } + } + } + + // Handling group Resources + if message.Type == "event" && message.Resource == "groups" && message.Event == "changed" { + + for _, l := range d.allDeconzDevices { + if l.Type == GroupDeconzDeviceType { + if fmt.Sprint(l.Group.ID) == message.ID { + log.WithField("Group", l.Group.Name).Debug("Deconz Websocket changed event for group") + l.stateChangeHandler(&message.State) + break + } + + } + + } + + } + + // Handling sensor Resources + if message.Type == "event" && message.Resource == "sensors" && message.Event == "changed" { + + for _, l := range d.allDeconzDevices { + if l.Type == SensorDeconzDeviceType { + if fmt.Sprint(l.Sensor.ID) == message.ID { + // Send to all devices which handles this sensor + log.WithField("sensor", l.Sensor.Name).Debug("Deconz, Websocket changed event for sensor") + l.stateChangeHandler(&message.State) + } + } + } + } + } +} diff --git a/pkg/entities/entities.go b/pkg/entities/entities.go index a1e65f2..caa1fb0 100644 --- a/pkg/entities/entities.go +++ b/pkg/entities/entities.go @@ -37,7 +37,7 @@ type EntityStateData struct { // Add an attribute if not already available func (e *Entity) AddAttribute(name string, value interface{}) { - if e.Attributes[name] == nil { + if _, ok := e.Attributes[name]; !ok { log.WithFields(log.Fields{ "entity_id": e.Id, "attribute": name, diff --git a/pkg/entities/light.go b/pkg/entities/light.go index cdb7279..fec3543 100644 --- a/pkg/entities/light.go +++ b/pkg/entities/light.go @@ -1,5 +1,9 @@ package entities +import ( + log "github.com/sirupsen/logrus" +) + type LightEntityState string type LightEntityFeatures string type LightEntityAttributes string @@ -38,6 +42,11 @@ type LightEntity struct { } func NewLightEntity(id string, name LanguageText, area string) *LightEntity { + log.WithFields(log.Fields{ + "ID": id, + "Name": name, + "Area": area, + }).Debug(("Create new LightEntity")) lightEntity := LightEntity{} lightEntity.Id = id @@ -67,6 +76,9 @@ func (e *LightEntity) AddFeature(feature LightEntityFeatures) { e.AddAttribute(string(HueLightEntityAttribute), 0) e.AddAttribute(string(SaturationLightEntityAttribute), 0) + case DimLightEntityFeatures: + e.AddAttribute(string(BrightnessLightEntityAttribute), 0) + case ColorTemperatureLightEntityFeatures: e.AddAttribute(string(ColorTemperatureLightEntityAttribute), 0) @@ -87,3 +99,9 @@ func (e *LightEntity) HandleCommand(cmd_id string, params map[string]interface{} return 404 } + +func (e *LightEntity) HasAttribute(attribute LightEntityAttributes) bool { + _, ok := e.Attributes[string(attribute)] + + return ok +} diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go index c117296..a6df2a6 100644 --- a/pkg/integration/integration.go +++ b/pkg/integration/integration.go @@ -53,9 +53,8 @@ func NewIntegration(config Config) (*Integration, error) { } - i.loadSetupData() - i.Remote.messageChannel = make(chan []byte) + i.Remote.controlChannel = make(chan string) return &i, nil @@ -64,6 +63,8 @@ func NewIntegration(config Config) (*Integration, error) { func (i *Integration) SetMetadata(metadata *DriverMetadata) { log.WithField("Metadata", metadata).Debug("Set Metadata") i.Metadata = metadata + + i.LoadSetupData() } func (i *Integration) Run() error { @@ -132,9 +133,9 @@ func (i *Integration) SetDriverSetupState(event_Type DriverSetupEventType, state // Load persist setupData File // TODO: handle location via ENV's -func (i *Integration) loadSetupData() { +func (i *Integration) LoadSetupData() { - file, err := os.ReadFile("ucrt.json") + file, err := os.ReadFile(i.Metadata.DriverId + ".json") if err != nil { log.WithError(err).Info("Cannot read setupDataFile") i.SetupData = make(SetupData) @@ -146,9 +147,9 @@ func (i *Integration) loadSetupData() { // Persist File // TODO: handle location via ENV's -func (i *Integration) persistSetupData() { +func (i *Integration) PersistSetupData() { log.WithField("SetupData", i.SetupData).Info("Persist setup data") file, _ := json.MarshalIndent(i.SetupData, "", " ") - _ = os.WriteFile("ucrt.json", file, 0644) + _ = os.WriteFile(i.Metadata.DriverId+".json", file, 0644) } diff --git a/pkg/integration/register.go b/pkg/integration/register.go index 424a704..4759876 100644 --- a/pkg/integration/register.go +++ b/pkg/integration/register.go @@ -136,7 +136,7 @@ func (i *Integration) registerWithRemoteTwo(remoteTwoIP string, remoteTwoPort in json.Unmarshal(resBody, &driverRegistration) i.SetupData["driver_id"] = driverRegistration.DriverId - i.persistSetupData() + i.PersistSetupData() } } diff --git a/pkg/integration/remote.go b/pkg/integration/remote.go index dc5f4cd..a8bf452 100644 --- a/pkg/integration/remote.go +++ b/pkg/integration/remote.go @@ -8,6 +8,9 @@ type remote struct { standby bool // Channel to send new messages over websocket. messageChannel chan []byte + + // Channel to close a Websocket + controlChannel chan string } func (r *remote) EnterStandBy() { diff --git a/pkg/integration/requests.go b/pkg/integration/requests.go index e791bfc..e578f53 100644 --- a/pkg/integration/requests.go +++ b/pkg/integration/requests.go @@ -189,7 +189,7 @@ func (i *Integration) handleSetupDriverRequest(req *SetupDriverMessageReq) *Resp i.SetupData = req.MsgData.Value - i.persistSetupData() + i.PersistSetupData() if i.handleSetupFunction != nil { // The handleSetupFunction is where the driver specific implmenentation for driver setup is diff --git a/pkg/integration/settingtypes.go b/pkg/integration/settingtypes.go index 0681436..dd6a3e8 100644 --- a/pkg/integration/settingtypes.go +++ b/pkg/integration/settingtypes.go @@ -56,8 +56,8 @@ type SettingTypeDropdowDefinition struct { Items []SettingTypeDropdowItemsDefinition `json:"items` } type SettingTypeDropdowItemsDefinition struct { - id string `json:"id"` - label LanguageText `json:"label"` + Id string `json:"id"` + Label LanguageText `json:"label"` } type SettingTypeLabel struct { diff --git a/pkg/integration/types.go b/pkg/integration/types.go index 6decedb..f033df5 100644 --- a/pkg/integration/types.go +++ b/pkg/integration/types.go @@ -47,7 +47,7 @@ type DriverMetadata struct { MinCoreAPI string `json:"min_core_api,omitempty"` Icon string `json:"icon,omitempty"` Description LanguageText `json:"description"` - Developer Developer `json:"description,omitempty"` + Developer Developer `json:"developer,omitempty"` HomePage string `json:"home_page,omitempty"` DeviceDiscovery bool `json:"device_discovery,omitempty"` SetupDataSchema SetupDataSchema `json:"setup_data_schema,omitempty"` diff --git a/pkg/integration/websocket.go b/pkg/integration/websocket.go index cb02343..221db0d 100644 --- a/pkg/integration/websocket.go +++ b/pkg/integration/websocket.go @@ -61,6 +61,9 @@ func (i *Integration) wsReader(ws *websocket.Conn) { defer func() { log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Closing Websocket, not able to read message anymore") ws.Close() + + // Close Write loop also + i.Remote.controlChannel <- ws.RemoteAddr().String() }() for { @@ -103,14 +106,23 @@ func (i *Integration) wsWriter(ws *websocket.Conn) { ticker := time.NewTicker(pingPeriod) defer func() { - log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Closing Websocket, no response in time to Ping message") + log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Closing Websocket") ws.Close() ticker.Stop() + // Close Read loop + i.Remote.controlChannel <- ws.RemoteAddr().String() }() for { select { + case msg := <-i.Remote.controlChannel: + // Close the writer if message was for this websocket. Closed by reader + if ws.RemoteAddr().String() == msg { + log.Debug("Closing write loop as read loop closed") + return + } + case msg := <-i.Remote.messageChannel: // Remote should not be in standby as this is a response to a request @@ -131,7 +143,7 @@ func (i *Integration) wsWriter(ws *websocket.Conn) { ws.SetWriteDeadline(time.Now().Add(writeWait)) log.WithField("RemoteAddr", ws.RemoteAddr().String()).Debug("Send Ping Message") if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil { - log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Could not send Ping message to") + log.WithField("RemoteAddr", ws.RemoteAddr().String()).Info("Could not send Ping message") return } }