From abb79e4b75660d145751c1f365f3735a3948918e Mon Sep 17 00:00:00 2001 From: Markus Thierolf <77847348+thierolm@users.noreply.github.com> Date: Fri, 18 Aug 2023 08:12:35 +0200 Subject: [PATCH] Tasmota: add cache (#9422) --- charger/tasmota.go | 113 ++----------------------- meter/tasmota.go | 10 ++- meter/tasmota/connection.go | 162 +++++++++++++++++++++++++++++++----- 3 files changed, 159 insertions(+), 126 deletions(-) diff --git a/charger/tasmota.go b/charger/tasmota.go index 6961bdf219..a0f0a31793 100644 --- a/charger/tasmota.go +++ b/charger/tasmota.go @@ -1,9 +1,7 @@ package charger import ( - "errors" - "fmt" - "strings" + "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/meter/tasmota" @@ -35,20 +33,22 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) { Password string StandbyPower float64 Channel int + Cache time.Duration }{ Channel: 1, + Cache: time.Second, } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewTasmota(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower) + return NewTasmota(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower, cc.Cache) } // NewTasmota creates Tasmota charger -func NewTasmota(embed embed, uri, user, password string, channel int, standbypower float64) (*Tasmota, error) { - conn, err := tasmota.NewConnection(uri, user, password, channel) +func NewTasmota(embed embed, uri, user, password string, channel int, standbypower float64, cache time.Duration) (*Tasmota, error) { + conn, err := tasmota.NewConnection(uri, user, password, channel, cache) if err != nil { return nil, err } @@ -60,112 +60,17 @@ func NewTasmota(embed embed, uri, user, password string, channel int, standbypow c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower) - return c, c.channelExists(channel) -} - -// channelExists checks the existence of the configured relay channel interface -func (c *Tasmota) channelExists(channel int) error { - var res *tasmota.StatusSTSResponse - if err := c.conn.ExecCmd("Status 0", &res); err != nil { - return err - } - - var ok bool - switch channel { - case 1: - ok = res.StatusSTS.Power != "" || res.StatusSTS.Power1 != "" - case 2: - ok = res.StatusSTS.Power2 != "" - case 3: - ok = res.StatusSTS.Power3 != "" - case 4: - ok = res.StatusSTS.Power4 != "" - case 5: - ok = res.StatusSTS.Power5 != "" - case 6: - ok = res.StatusSTS.Power6 != "" - case 7: - ok = res.StatusSTS.Power7 != "" - case 8: - ok = res.StatusSTS.Power8 != "" - } - - if !ok { - return fmt.Errorf("invalid relay channel: %d", channel) - } - - return nil + return c, c.conn.ChannelExists(channel) } // Enabled implements the api.Charger interface func (c *Tasmota) Enabled() (bool, error) { - var res tasmota.StatusSTSResponse - err := c.conn.ExecCmd("Status 0", &res) - if err != nil { - return false, err - } - - switch c.channel { - case 2: - return strings.ToUpper(res.StatusSTS.Power2) == "ON", err - case 3: - return strings.ToUpper(res.StatusSTS.Power3) == "ON", err - case 4: - return strings.ToUpper(res.StatusSTS.Power4) == "ON", err - case 5: - return strings.ToUpper(res.StatusSTS.Power5) == "ON", err - case 6: - return strings.ToUpper(res.StatusSTS.Power6) == "ON", err - case 7: - return strings.ToUpper(res.StatusSTS.Power7) == "ON", err - case 8: - return strings.ToUpper(res.StatusSTS.Power8) == "ON", err - default: - return strings.ToUpper(res.StatusSTS.Power) == "ON" || strings.ToUpper(res.StatusSTS.Power1) == "ON", err - } + return c.conn.Enabled() } // Enable implements the api.Charger interface func (c *Tasmota) Enable(enable bool) error { - var res tasmota.PowerResponse - - cmd := fmt.Sprintf("Power%d off", c.channel) - if enable { - cmd = fmt.Sprintf("Power%d on", c.channel) - } - - if err := c.conn.ExecCmd(cmd, &res); err != nil { - return err - } - - var on bool - switch c.channel { - case 2: - on = strings.ToUpper(res.Power2) == "ON" - case 3: - on = strings.ToUpper(res.Power3) == "ON" - case 4: - on = strings.ToUpper(res.Power4) == "ON" - case 5: - on = strings.ToUpper(res.Power5) == "ON" - case 6: - on = strings.ToUpper(res.Power6) == "ON" - case 7: - on = strings.ToUpper(res.Power7) == "ON" - case 8: - on = strings.ToUpper(res.Power8) == "ON" - default: - on = strings.ToUpper(res.Power) == "ON" || strings.ToUpper(res.Power1) == "ON" - } - - switch { - case enable && !on: - return errors.New("switchOn failed") - case !enable && on: - return errors.New("switchOff failed") - default: - return nil - } + return c.conn.Enable(enable) } var _ api.MeterEnergy = (*Tasmota)(nil) diff --git a/meter/tasmota.go b/meter/tasmota.go index b4faadee70..e069bbd182 100644 --- a/meter/tasmota.go +++ b/meter/tasmota.go @@ -1,6 +1,8 @@ package meter import ( + "time" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/meter/tasmota" "github.com/evcc-io/evcc/util" @@ -25,20 +27,22 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Meter, error) { Password string Channel int Usage string + Cache time.Duration }{ Channel: 1, + Cache: time.Second, } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewTasmota(cc.URI, cc.User, cc.Password, cc.Usage, cc.Channel) + return NewTasmota(cc.URI, cc.User, cc.Password, cc.Usage, cc.Channel, cc.Cache) } // NewTasmota creates Tasmota meter -func NewTasmota(uri, user, password, usage string, channel int) (*Tasmota, error) { - conn, err := tasmota.NewConnection(uri, user, password, channel) +func NewTasmota(uri, user, password, usage string, channel int, cache time.Duration) (*Tasmota, error) { + conn, err := tasmota.NewConnection(uri, user, password, channel, cache) if err != nil { return nil, err } diff --git a/meter/tasmota/connection.go b/meter/tasmota/connection.go index c94bec9e93..d55ab33e90 100644 --- a/meter/tasmota/connection.go +++ b/meter/tasmota/connection.go @@ -5,7 +5,9 @@ import ( "fmt" "net/url" "strings" + "time" + "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" @@ -16,10 +18,12 @@ type Connection struct { *request.Helper uri, user, password string channel int + statusSNSCache provider.Cacheable[StatusSNSResponse] + statusSTSCache provider.Cacheable[StatusSTSResponse] } // NewConnection creates a Tasmota connection -func NewConnection(uri, user, password string, channel int) (*Connection, error) { +func NewConnection(uri, user, password string, channel int, cache time.Duration) (*Connection, error) { if uri == "" { return nil, errors.New("missing uri") } @@ -35,46 +39,166 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error) c.Client.Transport = request.NewTripper(log, transport.Insecure()) + c.statusSNSCache = provider.ResettableCached(func() (StatusSNSResponse, error) { + parameters := url.Values{ + "user": []string{c.user}, + "password": []string{c.password}, + "cmnd": []string{"Status 8"}, + } + var res StatusSNSResponse + err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res) + return res, err + }, cache) + + c.statusSTSCache = provider.ResettableCached(func() (StatusSTSResponse, error) { + parameters := url.Values{ + "user": []string{c.user}, + "password": []string{c.password}, + "cmnd": []string{"Status 0"}, + } + var res StatusSTSResponse + err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res) + return res, err + }, cache) + return c, nil } -// ExecCmd executes an api command and provides the response -func (d *Connection) ExecCmd(cmd string, res interface{}) error { +// channelExists checks the existence of the configured relay channel interface +func (c *Connection) ChannelExists(channel int) error { + res, err := c.statusSTSCache.Get() + if err != nil { + return err + } + + var ok bool + switch channel { + case 1: + ok = res.StatusSTS.Power != "" || res.StatusSTS.Power1 != "" + case 2: + ok = res.StatusSTS.Power2 != "" + case 3: + ok = res.StatusSTS.Power3 != "" + case 4: + ok = res.StatusSTS.Power4 != "" + case 5: + ok = res.StatusSTS.Power5 != "" + case 6: + ok = res.StatusSTS.Power6 != "" + case 7: + ok = res.StatusSTS.Power7 != "" + case 8: + ok = res.StatusSTS.Power8 != "" + } + + if !ok { + return fmt.Errorf("invalid relay channel: %d", channel) + } + + return nil +} + +// Enable implements the api.Charger interface +func (c *Connection) Enable(enable bool) error { + cmd := fmt.Sprintf("Power%d off", c.channel) + if enable { + cmd = fmt.Sprintf("Power%d on", c.channel) + } + parameters := url.Values{ - "user": []string{d.user}, - "password": []string{d.password}, + "user": []string{c.user}, + "password": []string{c.password}, "cmnd": []string{cmd}, } - return d.GetJSON(fmt.Sprintf("%s/cm?%s", d.uri, parameters.Encode()), res) + var res PowerResponse + if err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res); err != nil { + return err + } + + var on bool + switch c.channel { + case 2: + on = strings.ToUpper(res.Power2) == "ON" + case 3: + on = strings.ToUpper(res.Power3) == "ON" + case 4: + on = strings.ToUpper(res.Power4) == "ON" + case 5: + on = strings.ToUpper(res.Power5) == "ON" + case 6: + on = strings.ToUpper(res.Power6) == "ON" + case 7: + on = strings.ToUpper(res.Power7) == "ON" + case 8: + on = strings.ToUpper(res.Power8) == "ON" + default: + on = strings.ToUpper(res.Power) == "ON" || strings.ToUpper(res.Power1) == "ON" + } + + c.statusSNSCache.Reset() + c.statusSTSCache.Reset() + + switch { + case enable && !on: + return errors.New("switchOn failed") + case !enable && on: + return errors.New("switchOff failed") + default: + return nil + } +} + +// Enabled implements the api.Charger interface +func (c *Connection) Enabled() (bool, error) { + res, err := c.statusSTSCache.Get() + if err != nil { + return false, err + } + + switch c.channel { + case 2: + return strings.ToUpper(res.StatusSTS.Power2) == "ON", err + case 3: + return strings.ToUpper(res.StatusSTS.Power3) == "ON", err + case 4: + return strings.ToUpper(res.StatusSTS.Power4) == "ON", err + case 5: + return strings.ToUpper(res.StatusSTS.Power5) == "ON", err + case 6: + return strings.ToUpper(res.StatusSTS.Power6) == "ON", err + case 7: + return strings.ToUpper(res.StatusSTS.Power7) == "ON", err + case 8: + return strings.ToUpper(res.StatusSTS.Power8) == "ON", err + default: + return strings.ToUpper(res.StatusSTS.Power) == "ON" || strings.ToUpper(res.StatusSTS.Power1) == "ON", err + } } // CurrentPower implements the api.Meter interface -func (d *Connection) CurrentPower() (float64, error) { - var res StatusSNSResponse - if err := d.ExecCmd("Status 8", &res); err != nil { +func (c *Connection) CurrentPower() (float64, error) { + res, err := c.statusSNSCache.Get() + if err != nil { return 0, err } - return res.StatusSNS.Energy.Power.Channel(d.channel) + return res.StatusSNS.Energy.Power.Channel(c.channel) } // TotalEnergy implements the api.MeterEnergy interface -func (d *Connection) TotalEnergy() (float64, error) { - var res StatusSNSResponse - err := d.ExecCmd("Status 8", &res) +func (c *Connection) TotalEnergy() (float64, error) { + res, err := c.statusSNSCache.Get() return res.StatusSNS.Energy.Total, err } // SmlPower provides the sml sensor power -func (d *Connection) SmlPower() (float64, error) { - var res StatusSNSResponse - err := d.ExecCmd("Status 8", &res) +func (c *Connection) SmlPower() (float64, error) { + res, err := c.statusSNSCache.Get() return float64(res.StatusSNS.SML.PowerCurr), err } // SmlTotalEnergy provides the sml sensor total import energy -func (d *Connection) SmlTotalEnergy() (float64, error) { - var res StatusSNSResponse - err := d.ExecCmd("Status 8", &res) +func (c *Connection) SmlTotalEnergy() (float64, error) { + res, err := c.statusSNSCache.Get() return res.StatusSNS.SML.TotalIn, err }