Skip to content

Commit

Permalink
feat: implement Tasmota client
Browse files Browse the repository at this point in the history
  • Loading branch information
splattner committed Oct 3, 2023
1 parent 6e631a4 commit c8dbbbe
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 61 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/client/deconzclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = "%"
}

}
Expand Down
131 changes: 107 additions & 24 deletions pkg/client/tasmotaclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
type TasmotaClient struct {
Client
tasmota *tasmota.Tasmota

mapOnState map[string]entities.LightEntityState
}

func NewTasmotaClient(i *integration.Integration) *TasmotaClient {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -183,22 +190,27 @@ 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)

switchEntity.MapCommand(entities.OnSwitchEntityCommand, device.TurnOn)
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)
Expand All @@ -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

}

Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/tasmota/tasmota.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/ucrt/ucrt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -99,6 +100,7 @@ func NewCommand(name string) *cobra.Command {
denonavr.NewCommand(rootCmd),
deconz.NewCommand(rootCmd),
shelly.NewCommand(rootCmd),
tasmota.NewCommand(rootCmd),
)

return rootCmd
Expand Down
26 changes: 19 additions & 7 deletions pkg/entities/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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{}) {

Expand Down
Loading

0 comments on commit c8dbbbe

Please sign in to comment.