From c8dbbbebba191316343c3ce7a71818b1ef9f508a Mon Sep 17 00:00:00 2001 From: Sebastian Plattner Date: Tue, 3 Oct 2023 20:55:43 +0200 Subject: [PATCH] feat: implement Tasmota client --- README.md | 13 +++- pkg/client/deconzclient.go | 2 + pkg/client/tasmotaclient.go | 131 ++++++++++++++++++++++++++++------- pkg/cmd/tasmota/tasmota.go | 2 +- pkg/cmd/ucrt/ucrt.go | 2 + pkg/entities/entities.go | 26 +++++-- pkg/integration/entities.go | 97 ++++++++++++++++++++++++++ pkg/integration/requests.go | 9 +++ pkg/integration/websocket.go | 3 + pkg/tasmota/device.go | 103 +++++++++++++++++++++------ pkg/tasmota/mqtt.go | 4 ++ pkg/tasmota/types.go | 15 ++-- 12 files changed, 346 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 54f364a..0224c64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # A Unfolded Circle Remote Two Integration in Go -![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/splattner/goucrt/docker-publish.yml) +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/splattner/goucrt/main.yaml) ![GitHub](https://img.shields.io/github/license/splattner/goucrt) ![GitHub (Pre-)Release Date](https://img.shields.io/github/release-date-pre/splattner/goucrt) @@ -38,6 +38,17 @@ This client currently implements [`Light` entities](https://github.com/unfoldedc This client currently implements [`Switch` entities](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/switch_light.md) for discovered Shelly Devices. It uses MQTT to discover and control Shelly devices. +### Tasmota + +This client currently implements [`Switch` entities](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/switch_light.md) and [`Light` entities](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_light.md) It uses MQTT to discover and control Tasmota devices. + +Currently on the following Sonoff device types are supported + +* `0` Sonoff Basic results in a Switch entity +* `4` RGBW results in a Light entity + +*Note* The light entity does not really suport RGBW. So currently, as a somehow working workaround, when setting the brithness is set to 0, the entity changes between RGB & W Settings. + ## How to use ```bash diff --git a/pkg/client/deconzclient.go b/pkg/client/deconzclient.go index 4a6dbf0..e7045d5 100644 --- a/pkg/client/deconzclient.go +++ b/pkg/client/deconzclient.go @@ -217,11 +217,13 @@ func (c *DeconzClient) handleNewSensorDeviceDiscovered(device *deconz.DeconzDevi case entities.TemperaturSensorDeviceClass: if state.Temperature != nil { attributes["value"] = *state.Temperature / int16(100.0) + attributes["unit"] = "°C" } case entities.HumiditySensorDeviceClass: if state.Humidity != nil { attributes["value"] = *state.Humidity / uint16(100) + attributes["unit"] = "%" } } diff --git a/pkg/client/tasmotaclient.go b/pkg/client/tasmotaclient.go index 8f4d772..0c4578b 100644 --- a/pkg/client/tasmotaclient.go +++ b/pkg/client/tasmotaclient.go @@ -16,6 +16,8 @@ import ( type TasmotaClient struct { Client tasmota *tasmota.Tasmota + + mapOnState map[string]entities.LightEntityState } func NewTasmotaClient(i *integration.Integration) *TasmotaClient { @@ -102,6 +104,11 @@ func NewTasmotaClient(i *integration.Integration) *TasmotaClient { tasmota.clientLoopFunc = tasmota.tasmotaClientLoop //client.setDriverUserDataFunc = client.handleSetDriverUserData + tasmota.mapOnState = map[string]entities.LightEntityState{ + "ON": entities.OnLightEntityState, + "OFF": entities.OffLightEntityState, + } + return &tasmota } @@ -183,6 +190,10 @@ func (c *TasmotaClient) handleNewDeviceDiscovered(device *tasmota.TasmotaDevice) case 0: // Sonoff Basic switchEntity := entities.NewSwitchEntity(device.Topic, entities.LanguageText{En: "Tasmota " + device.FriendlyName[0]}, "") + + switchEntity.SubscribeCallbackFunc = device.Subscribe + switchEntity.UnsubscribeCallbackFunc = device.Unsubscribe + switchEntity.AddFeature(entities.OnOffSwitchEntityyFeatures) switchEntity.AddFeature(entities.ToggleSwitchEntityyFeatures) @@ -190,15 +201,16 @@ func (c *TasmotaClient) handleNewDeviceDiscovered(device *tasmota.TasmotaDevice) switchEntity.MapCommand(entities.OffSwitchEntityCommand, device.TurnOff) switchEntity.MapCommand(entities.ToggleSwitchEntityCommand, device.Toggle) - device.AddMsgReceivedFunc("RESULT", func(msg []byte) { + device.AddMsgReceivedFunc("RESULT", func(msg interface{}) { + + res := msg.(tasmota.TasmotaResultMsg) attributes := make(map[string]interface{}) - switch string(msg) { - case "on": - attributes[string(entities.StateSwitchEntityyAttribute)] = entities.OnSwitchtEntityState - case "off": - attributes[string(entities.StateSwitchEntityyAttribute)] = entities.OffSwitchtEntityState + if res.Power == "ON" || res.Power1 == "ON" { + attributes[string(entities.StateLightEntityAttribute)] = entities.OnLightEntityState + } else { + attributes[string(entities.StateLightEntityAttribute)] = entities.OffLightEntityState } switchEntity.SetAttributes(attributes) @@ -208,32 +220,100 @@ func (c *TasmotaClient) handleNewDeviceDiscovered(device *tasmota.TasmotaDevice) case 4: // RGBW - lightEntity := entities.NewLightEntity(device.Topic, entities.LanguageText{En: "Tasmota " + device.FriendlyName[0]}, "") + lightEntity_rgb := entities.NewLightEntity(device.Topic, entities.LanguageText{En: "Tasmota " + device.FriendlyName[0]}, "") + + lightEntity_rgb.SubscribeCallbackFunc = device.Subscribe + lightEntity_rgb.UnsubscribeCallbackFunc = device.Unsubscribe + + lightEntity_rgb.AddFeature(entities.OnOffLightEntityFeatures) + lightEntity_rgb.AddFeature(entities.ToggleLightEntityFeatures) + lightEntity_rgb.AddFeature(entities.DimLightEntityFeatures) + lightEntity_rgb.AddFeature(entities.ColorLightEntityFeatures) + + // Add commands + lightEntity_rgb.AddCommand(entities.OnLightEntityCommand, func(entity entities.LightEntity, params map[string]interface{}) int { + + // NO param set, so just turn on + if len(params) == 0 { + if err := device.TurnOn(); err != nil { + return 404 + } + } else { + if params["saturation"] != nil && params["hue"] != nil { + + hue := float32(params["hue"].(float64)) + sat := float32(params["saturation"].(float64) / 255 * 100) + + // Color Light + if err := device.SetHue(hue); err != nil { + return 404 + } + if err := device.SetSaturation(sat); err != nil { + return 404 + } + + } + + if params["brightness"] != nil { + bri := int(params["brightness"].(float64) / 255 * 100) + if bri > 0 && device.LocalState.White == 0 { + // Set Brightness if not in White mode + if err := device.SetBrightness(bri); err != nil { + return 404 + } + } else { + + // When in color mode, and bri is 0, set/turn on white mode + // Setting white to 0 turns off the white mode and return to color mode + if err := device.SetWhite(bri); err != nil { + return 404 + } + } + + } - lightEntity.AddFeature(entities.OnOffLightEntityFeatures) - lightEntity.AddFeature(entities.ToggleLightEntityFeatures) - lightEntity.AddFeature(entities.DimLightEntityFeatures) - lightEntity.AddFeature(entities.ColorLightEntityFeatures) + } + + return 200 + }) - lightEntity.MapCommand(entities.OnLightEntityCommand, device.TurnOn) - lightEntity.MapCommand(entities.OffLightEntityCommand, device.TurnOff) - lightEntity.MapCommand(entities.ToggleLightEntityCommand, device.Toggle) + lightEntity_rgb.MapCommand(entities.OffLightEntityCommand, device.TurnOff) + lightEntity_rgb.MapCommand(entities.ToggleLightEntityCommand, device.Toggle) - device.AddMsgReceivedFunc("RESULT", func(msg []byte) { + device.AddMsgReceivedFunc("RESULT", func(msg interface{}) { + + res := msg.(tasmota.TasmotaResultMsg) + + log.WithFields(log.Fields{"res": res, "Device": device.FriendlyName}).Debug("Result msg received") attributes := make(map[string]interface{}) - switch string(msg) { - case "on": + if res.Power == "ON" || res.Power1 == "ON" { attributes[string(entities.StateLightEntityAttribute)] = entities.OnLightEntityState - case "off": + } else { attributes[string(entities.StateLightEntityAttribute)] = entities.OffLightEntityState } - lightEntity.SetAttributes(attributes) + // Only White light + if res.White > 0 { + attributes[string(entities.SaturationLightEntityAttribute)] = 0 + attributes[string(entities.BrightnessLightEntityAttribute)] = int(float32(res.White) / 100 * 255) + } else { + if res.HSBCOlor != "" { + // Handle COlor Part of light + hue, sat, bri := device.GetHSB(res.HSBCOlor) + + attributes[string(entities.HueLightEntityAttribute)] = int(hue) + attributes[string(entities.SaturationLightEntityAttribute)] = int(float64(sat) / 100 * 255) + attributes[string(entities.BrightnessLightEntityAttribute)] = int(float64(bri) / 100 * 255) + + } + } + + lightEntity_rgb.SetAttributes(attributes) }) - tasmotaDevice = lightEntity + tasmotaDevice = lightEntity_rgb } @@ -256,16 +336,19 @@ func (c *TasmotaClient) handleNewDeviceDiscovered(device *tasmota.TasmotaDevice) // Callen on RT connect func (c *TasmotaClient) tasmotaClientLoop() { + ticker := time.NewTicker(5 * time.Minute) + defer func() { - c.tasmota.StopDiscovery() - c.tasmota.Stop() + if c.tasmota != nil { + c.tasmota.StopDiscovery() + c.tasmota.Stop() + } + ticker.Stop() c.setDeviceState(integration.DisconnectedDeviceState) }() if c.tasmota == nil { c.setupTasmota() - } else { - return } if c.tasmota != nil { diff --git a/pkg/cmd/tasmota/tasmota.go b/pkg/cmd/tasmota/tasmota.go index 44303c7..356a89b 100644 --- a/pkg/cmd/tasmota/tasmota.go +++ b/pkg/cmd/tasmota/tasmota.go @@ -37,7 +37,7 @@ func NewCommand(rootCmd *cobra.Command) *cobra.Command { i, err := integration.NewIntegration(config) cmd.CheckError(err) - myclient := client.NewShellyClient(i) + myclient := client.NewTasmotaClient(i) myclient.InitClient() diff --git a/pkg/cmd/ucrt/ucrt.go b/pkg/cmd/ucrt/ucrt.go index f6b2714..8b33094 100644 --- a/pkg/cmd/ucrt/ucrt.go +++ b/pkg/cmd/ucrt/ucrt.go @@ -6,6 +6,7 @@ import ( "github.com/splattner/goucrt/pkg/cmd/deconz" "github.com/splattner/goucrt/pkg/cmd/denonavr" "github.com/splattner/goucrt/pkg/cmd/shelly" + "github.com/splattner/goucrt/pkg/cmd/tasmota" log "github.com/sirupsen/logrus" ) @@ -99,6 +100,7 @@ func NewCommand(name string) *cobra.Command { denonavr.NewCommand(rootCmd), deconz.NewCommand(rootCmd), shelly.NewCommand(rootCmd), + tasmota.NewCommand(rootCmd), ) return rootCmd diff --git a/pkg/entities/entities.go b/pkg/entities/entities.go index 423a114..f0fb6b2 100644 --- a/pkg/entities/entities.go +++ b/pkg/entities/entities.go @@ -16,13 +16,15 @@ const ( type Entity struct { Id string `json:"entity_id"` EntityType - DeviceId string `json:"device_id,omitempty"` - Features []interface{} `json:"features"` - Name LanguageText `json:"name"` - Area string `json:"area,omitempty"` - DeviceClass string `json:"-"` - Attributes map[string]interface{} `json:"-"` - handleEntityChangeFunc func(interface{}, *map[string]interface{}) `json:"-"` + DeviceId string `json:"device_id,omitempty"` + Features []interface{} `json:"features"` + Name LanguageText `json:"name"` + Area string `json:"area,omitempty"` + DeviceClass string `json:"-"` + Attributes map[string]interface{} `json:"-"` + handleEntityChangeFunc func(interface{}, *map[string]interface{}) `json:"-"` + SubscribeCallbackFunc func() `json:"-"` + UnsubscribeCallbackFunc func() `json:"-"` } type EntityType struct { @@ -81,6 +83,16 @@ func (e *Entity) SetHandleEntityChangeFunc(f func(interface{}, *map[string]inter e.handleEntityChangeFunc = f } +// Set the function that is called when RT subscribes to this entity +func (e *Entity) SetSubscribeCallbackFunc(f func()) { + e.SubscribeCallbackFunc = f +} + +// Set the function that is called when RT unsubscribes to this entity +func (e *Entity) SetUnsubscribeCallbackFunc(f func()) { + e.UnsubscribeCallbackFunc = f +} + // Set attributes for the Entity and then call the EntityChange Function func (e *Entity) SetAttributes(attributes map[string]interface{}) { diff --git a/pkg/integration/entities.go b/pkg/integration/entities.go index 33b9485..80ae404 100644 --- a/pkg/integration/entities.go +++ b/pkg/integration/entities.go @@ -5,6 +5,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/splattner/goucrt/pkg/entities" + "k8s.io/utils/strings/slices" ) // Return the ID of an entity @@ -148,6 +149,12 @@ func (i *Integration) AddEntity(e interface{}) error { i.Entities = append(i.Entities, e) // Send "entity_available" event to remote i.sendEntityAvailable(e) + + // if RT already subscribed, call the Subscribe callback for this entity + if i.isSubscribed(e) { + i.callSubscribeCallback(e) + } + return nil } @@ -155,6 +162,12 @@ func (i *Integration) AddEntity(e interface{}) error { return i.UpdateEntity(existingEntity, e) } +func (i *Integration) isSubscribed(entity interface{}) bool { + entity_id := i.getEntityId(entity) + + return slices.Contains(i.SubscribedEntities, entity_id) +} + // Update an existing entity with a new entity func (i *Integration) UpdateEntity(entity interface{}, newEntity interface{}) error { switch e := entity.(type) { @@ -295,3 +308,87 @@ func (i *Integration) setEntityChangeFunc(entity interface{}, f func(interface{} e.SetHandleEntityChangeFunc(f) } } + +// Call the correct subscribe Callback Func depending on the entity type +func (i *Integration) callSubscribeCallback(entity interface{}) { + + // Ugly.. I guess but I don't know how better + switch e := entity.(type) { + case *entities.ButtonEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.LightEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.SwitchsEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.MediaPlayerEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.ClimateEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.CoverEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.SensorEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + } +} + +// Call the correct unsubscribe Callback Func depending on the entity type +func (i *Integration) callUnubscribeCallback(entity interface{}) { + + // Ugly.. I guess but I don't know how better + switch e := entity.(type) { + case *entities.ButtonEntity: + if e.UnsubscribeCallbackFunc != nil { + e.UnsubscribeCallbackFunc() + } + + case *entities.LightEntity: + if e.UnsubscribeCallbackFunc != nil { + e.UnsubscribeCallbackFunc() + } + + case *entities.SwitchsEntity: + if e.UnsubscribeCallbackFunc != nil { + e.UnsubscribeCallbackFunc() + } + + case *entities.MediaPlayerEntity: + if e.UnsubscribeCallbackFunc != nil { + e.UnsubscribeCallbackFunc() + } + + case *entities.ClimateEntity: + if e.UnsubscribeCallbackFunc != nil { + e.UnsubscribeCallbackFunc() + } + + case *entities.CoverEntity: + if e.SubscribeCallbackFunc != nil { + e.SubscribeCallbackFunc() + } + + case *entities.SensorEntity: + if e.UnsubscribeCallbackFunc != nil { + e.UnsubscribeCallbackFunc() + } + } +} diff --git a/pkg/integration/requests.go b/pkg/integration/requests.go index 8cb5414..d1ade9f 100644 --- a/pkg/integration/requests.go +++ b/pkg/integration/requests.go @@ -244,6 +244,7 @@ func (i *Integration) handleSubscribeEventRequest(req *SubscribeEventMessageReq) if !slices.Contains(i.SubscribedEntities, entity_id) { log.WithField("entity_id", entity_id).Info("RT subscribed to entity") i.SubscribedEntities = append(i.SubscribedEntities, entity_id) + i.callSubscribeCallback(e) } } @@ -253,6 +254,10 @@ func (i *Integration) handleSubscribeEventRequest(req *SubscribeEventMessageReq) if !slices.Contains(i.SubscribedEntities, entity_id) { log.WithField("entity_id", entity_id).Info("RT subscribed to entity") i.SubscribedEntities = append(i.SubscribedEntities, entity_id) + + if entity, _, err := i.GetEntityById(entity_id); err != nil { + i.callSubscribeCallback(entity) + } } } } @@ -278,6 +283,10 @@ func (i *Integration) handleUnsubscribeEventsRequest(req *UnubscribeEventMessage i.SubscribedEntities[ix] = i.SubscribedEntities[len(i.SubscribedEntities)-1] // Copy last element to index i. i.SubscribedEntities[len(i.SubscribedEntities)-1] = "" // Erase last element (write zero value). i.SubscribedEntities = i.SubscribedEntities[:len(i.SubscribedEntities)-1] // Truncate slice. + + if entity, _, err := i.GetEntityById(e); err != nil { + i.callUnubscribeCallback(entity) + } } } diff --git a/pkg/integration/websocket.go b/pkg/integration/websocket.go index 259dcc4..b919765 100644 --- a/pkg/integration/websocket.go +++ b/pkg/integration/websocket.go @@ -138,16 +138,19 @@ func (i *Integration) wsWriter(ws *websocket.Conn) { if err := ws.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { log.WithError(err).Error("Faled to set WriteDeatLine") + return } if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil { log.WithError(err).Error("Failed to send message") + return } case <-ticker.C: if err := ws.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { log.WithError(err).Error("Cannot set writedealine") + return } log.WithField("RemoteAddr", ws.RemoteAddr().String()).Debug("Send Ping Message") if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil { diff --git a/pkg/tasmota/device.go b/pkg/tasmota/device.go index dd873b7..92cdc49 100644 --- a/pkg/tasmota/device.go +++ b/pkg/tasmota/device.go @@ -3,6 +3,7 @@ package tasmota import ( "encoding/json" "fmt" + "strconv" "strings" mqtt "github.com/eclipse/paho.mqtt.golang" @@ -37,39 +38,37 @@ type TasmotaDevice struct { ShutterOptions []int `json:"sho,omitempty"` Version int `json:"ver,omitempty"` - LastResultMessage TasmotaResultMsg - LastTeleMessame TasmotaTeleMsg + LocalState TasmotaResultMsg + LastTeleMessame TasmotaTeleMsg - PowerState bool - - handleMsgReceivedFunc map[string][]func([]byte) + handleMsgReceivedFunc map[string][]func(interface{}) } func (d *TasmotaDevice) newTasmotaDevice(tasmota *Tasmota) { d.tasmota = tasmota - d.configureCallbacks() + //d.subscribe() - d.handleMsgReceivedFunc = make(map[string][]func([]byte)) + d.handleMsgReceivedFunc = make(map[string][]func(interface{})) } // Add a function that is called when a message is eceiverd from a Shelly device on a selected topic -func (d *TasmotaDevice) AddMsgReceivedFunc(topic string, f func(payload []byte)) { +func (d *TasmotaDevice) AddMsgReceivedFunc(topic string, f func(message interface{})) { d.handleMsgReceivedFunc[topic] = append(d.handleMsgReceivedFunc[topic], f) } // Call all MsgReceivedFunc for this device and topic -func (d *TasmotaDevice) stateChangeHandler(topic string, payload []byte) { +func (d *TasmotaDevice) stateChangeHandler(topic string, message interface{}) { if d.handleMsgReceivedFunc[topic] != nil { for _, f := range d.handleMsgReceivedFunc[topic] { - f(payload) + f(message) } } } -func (e *TasmotaDevice) configureCallbacks() { +func (e *TasmotaDevice) Subscribe() { log.WithField("Topic", e.Topic).Debug("Subscribe to Tasmota Topic for this device") // Add callback for stat @@ -79,6 +78,23 @@ func (e *TasmotaDevice) configureCallbacks() { // Add callback for tele topicTele := fmt.Sprintf("tele/%s/#", e.Topic) e.tasmota.subscribeMqttTopic(topicTele, e.mqttCallback()) + + // Make sure to have an initial State + if err := e.GetResult(); err != nil { + log.WithError(err).Debug("Cannot get a Result") + } +} + +func (e *TasmotaDevice) Unsubscribe() { + log.WithField("Topic", e.Topic).Debug("Subscribe to Tasmota Topic for this device") + + // Add callback for stat + topicStat := fmt.Sprintf("stat/%s/#", e.Topic) + e.tasmota.unsubscribeMqttTopic(topicStat) + + // Add callback for tele + topicTele := fmt.Sprintf("tele/%s/#", e.Topic) + e.tasmota.unsubscribeMqttTopic(topicTele) } func (d *TasmotaDevice) mqttCallback() mqtt.MessageHandler { @@ -95,16 +111,22 @@ func (d *TasmotaDevice) mqttCallback() mqtt.MessageHandler { switch topic { case "RESULT": - err := json.Unmarshal(msg.Payload(), &d.LastResultMessage) + var resultMessage TasmotaResultMsg + err := json.Unmarshal(msg.Payload(), &resultMessage) + if resultMessage.CustomSend == "Done" { + // We don't care for this message + return + } if err != nil { log.WithError(err).Debug("Unmarshal to TasmotaPowerMsg failed") return } + // Set internal state - d.PowerState = d.LastResultMessage.Power1 == "ON" || d.LastResultMessage.Power == "ON" + d.LocalState = resultMessage - d.stateChangeHandler(topic, msg.Payload()) + d.stateChangeHandler(topic, resultMessage) case "SENSOR": err := json.Unmarshal(msg.Payload(), &d.LastTeleMessame) if err != nil { @@ -122,15 +144,21 @@ func (d *TasmotaDevice) mqttCallback() mqtt.MessageHandler { } func (e *TasmotaDevice) TurnOn() error { - return e.tasmota.publishMqttCommand("shellies/"+e.Topic+"/relay/0/command", "on") + + if err := e.tasmota.publishMqttCommand("cmnd/"+e.Topic+"/POWER", "ON"); err != nil { + return err + } + + // TUrn on does not send the current state of the device, so force it + return e.GetResult() } func (e *TasmotaDevice) TurnOff() error { - return e.tasmota.publishMqttCommand("shellies/"+e.Topic+"/relay/0/command", "off") + return e.tasmota.publishMqttCommand("cmnd/"+e.Topic+"/POWER", "OFF") } func (e *TasmotaDevice) IsOn() bool { - return e.PowerState + return e.LocalState.Power == "ON" || e.LocalState.Power1 == "ON" } func (e *TasmotaDevice) Toggle() error { @@ -142,7 +170,11 @@ func (e *TasmotaDevice) Toggle() error { return e.TurnOn() } -func (d *TasmotaDevice) SetBrightness(brightness float32) error { +func (d *TasmotaDevice) GetResult() error { + return d.tasmota.publishMqttCommand("cmnd/"+d.Topic+"/HsbColor", "") +} + +func (d *TasmotaDevice) SetBrightness(brightness int) error { return d.tasmota.publishMqttCommand("cmnd/"+d.Topic+"/HsbColor3", brightness) } @@ -154,11 +186,11 @@ func (d *TasmotaDevice) SetSaturation(saturation float32) error { return d.tasmota.publishMqttCommand("cmnd/"+d.Topic+"/HsbColor2", saturation) } -func (d *TasmotaDevice) SetHSB(hue float32, saturation float32, brightness float32) error { - return d.tasmota.publishMqttCommand("cmnd/"+d.Topic+"/HsbColor", fmt.Sprintf("%.0f,%.0f,%.0f", hue, saturation, brightness)) +func (d *TasmotaDevice) SetHSB(hue float32, saturation float32, brightness int) error { + return d.tasmota.publishMqttCommand("cmnd/"+d.Topic+"/HsbColor", fmt.Sprintf("%.0f,%.0f,%d", hue, saturation, brightness)) } -func (d *TasmotaDevice) SetWhite(white float32) error { +func (d *TasmotaDevice) SetWhite(white int) error { //e.publishMqttCommand("cmnd/"+e.Topic+"/Color1", "0,0,0") return d.tasmota.publishMqttCommand("cmnd/"+d.Topic+"/White", white) } @@ -167,3 +199,32 @@ func (e *TasmotaDevice) SetColorTemp(ct float32) error { log.Warningln("Setting Color Temp not implemented") return nil } + +func (d *TasmotaDevice) GetHSB(color string) (float64, float64, int) { + + if color == "" { + color = d.LocalState.HSBCOlor + } + + hsb := strings.Split(color, ",") + + if len(hsb) == 3 { + + hue, err := strconv.Atoi(hsb[0]) + if err != nil { + log.WithError(err).Error("Unable to parse HSB") + } + sat, err := strconv.Atoi(hsb[1]) + if err != nil { + log.WithError(err).Error("Unable to parse HSB") + } + bri, err := strconv.Atoi(hsb[2]) + if err != nil { + log.WithError(err).Error("Unable to parse HSB") + } + + return float64(hue), float64(sat), bri + } + + return 0, 0, 0 +} diff --git a/pkg/tasmota/mqtt.go b/pkg/tasmota/mqtt.go index 8f613a5..c7ede33 100644 --- a/pkg/tasmota/mqtt.go +++ b/pkg/tasmota/mqtt.go @@ -8,6 +8,10 @@ import ( ) func (e *Tasmota) publishMqttCommand(topic string, value interface{}) error { + log.WithFields(log.Fields{ + "topic": topic, + "value": fmt.Sprintf("%v", value)}).Debug("Publish MQTT Command") + if token := e.mqttClient.Publish(topic, 0, false, fmt.Sprintf("%v", value)); token.Wait() && token.Error() != nil { log.WithError(token.Error()).Error("MQTT publish failed") return token.Error() diff --git a/pkg/tasmota/types.go b/pkg/tasmota/types.go index a8982ab..07c86cc 100644 --- a/pkg/tasmota/types.go +++ b/pkg/tasmota/types.go @@ -1,13 +1,14 @@ package tasmota type TasmotaResultMsg struct { - Power1 string `json:"POWER1,omitempty"` - Power string `json:"POWER,omitempty"` - Dimmer int `json:"Dimmer,omitempty"` - Color string `json:"Color,omitempty"` - HSBCOlor string `json:"HSBColor,omitempty"` - White int `json:"White,omitempty"` - Channel []int `json:"Channel,omitempty"` + Power1 string `json:"POWER1,omitempty"` + Power string `json:"POWER,omitempty"` + Dimmer int `json:"Dimmer,omitempty"` + Color string `json:"Color,omitempty"` + HSBCOlor string `json:"HSBColor,omitempty"` + White int `json:"White,omitempty"` + Channel []int `json:"Channel,omitempty"` + CustomSend string `json:"CustomSend,omitempty"` } type TasmotaTeleMsg struct {