Skip to content

Commit

Permalink
Tasmota: add cache (evcc-io#9422)
Browse files Browse the repository at this point in the history
  • Loading branch information
thierolm authored Aug 18, 2023
1 parent eb08e6d commit abb79e4
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 126 deletions.
113 changes: 9 additions & 104 deletions charger/tasmota.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package charger

import (
"errors"
"fmt"
"strings"
"time"

"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/meter/tasmota"
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down
10 changes: 7 additions & 3 deletions meter/tasmota.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
}
Expand Down
162 changes: 143 additions & 19 deletions meter/tasmota/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
}
Expand All @@ -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
}

0 comments on commit abb79e4

Please sign in to comment.