evcc-io/meter/tasmota/connection.go
2026-02-08 22:29:27 +01:00

298 lines
7.1 KiB
Go

package tasmota
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
)
// Connection is the Tasmota connection
type Connection struct {
*request.Helper
uri, user, password string
channels []int
statusSnsG util.Cacheable[StatusSNSResponse]
statusStsG util.Cacheable[StatusSTSResponse]
}
// NewConnection creates a Tasmota connection
func NewConnection(uri, user, password string, channels []int, cache time.Duration) (*Connection, error) {
if uri == "" {
return nil, errors.New("missing uri")
}
if l := len(channels); l != 1 && l != 3 {
return nil, fmt.Errorf("invalid number of channels: %d", l)
}
used := make(map[int]bool)
for _, c := range channels {
if c < 1 || c > 8 {
return nil, fmt.Errorf("invalid channel: %d", c)
}
if used[c] {
return nil, fmt.Errorf("duplicate channel: %d", c)
}
used[c] = true
}
log := util.NewLogger("tasmota")
c := &Connection{
Helper: request.NewHelper(log),
uri: util.DefaultScheme(strings.TrimRight(uri, "/"), "http"),
user: user,
password: password,
channels: channels,
}
c.Client.Transport = request.NewTripper(log, transport.Insecure())
c.statusSnsG = util.ResettableCached(func() (StatusSNSResponse, error) {
parameters := url.Values{
"user": {c.user},
"password": {c.password},
"cmnd": {"Status 8"},
}
var res StatusSNSResponse
err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res)
return res, err
}, cache)
c.statusStsG = util.ResettableCached(func() (StatusSTSResponse, error) {
parameters := url.Values{
"user": {c.user},
"password": {c.password},
"cmnd": {"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
}
// channelExists checks the existence of the configured relay channel interface
func (c *Connection) RelayExists() error {
res, err := c.statusStsG.Get()
if err != nil {
return err
}
var ok bool
for _, channel := range c.channels {
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 {
for _, channel := range c.channels {
cmd := fmt.Sprintf("Power%d off", channel)
if enable {
cmd = fmt.Sprintf("Power%d on", channel)
}
parameters := url.Values{
"user": {c.user},
"password": {c.password},
"cmnd": {cmd},
}
var res PowerResponse
if err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res); err != nil {
return err
}
var enabled bool
switch channel {
case 2:
enabled = strings.ToUpper(res.Power2) == "ON"
case 3:
enabled = strings.ToUpper(res.Power3) == "ON"
case 4:
enabled = strings.ToUpper(res.Power4) == "ON"
case 5:
enabled = strings.ToUpper(res.Power5) == "ON"
case 6:
enabled = strings.ToUpper(res.Power6) == "ON"
case 7:
enabled = strings.ToUpper(res.Power7) == "ON"
case 8:
enabled = strings.ToUpper(res.Power8) == "ON"
default:
enabled = strings.ToUpper(res.Power) == "ON" || strings.ToUpper(res.Power1) == "ON"
}
switch {
case enable && !enabled:
return errors.New("switchOn failed")
case !enable && enabled:
return errors.New("switchOff failed")
}
}
c.statusSnsG.Reset()
c.statusStsG.Reset()
return nil
}
// Enabled implements the api.Charger interface
func (c *Connection) Enabled() (bool, error) {
res, err := c.statusStsG.Get()
if err != nil {
return false, err
}
var enabled bool
for _, channel := range c.channels {
switch channel {
case 2:
enabled = strings.ToUpper(res.StatusSTS.Power2) == "ON"
case 3:
enabled = strings.ToUpper(res.StatusSTS.Power3) == "ON"
case 4:
enabled = strings.ToUpper(res.StatusSTS.Power4) == "ON"
case 5:
enabled = strings.ToUpper(res.StatusSTS.Power5) == "ON"
case 6:
enabled = strings.ToUpper(res.StatusSTS.Power6) == "ON"
case 7:
enabled = strings.ToUpper(res.StatusSTS.Power7) == "ON"
case 8:
enabled = strings.ToUpper(res.StatusSTS.Power8) == "ON"
default:
enabled = strings.ToUpper(res.StatusSTS.Power) == "ON" || strings.ToUpper(res.StatusSTS.Power1) == "ON"
}
}
return enabled, nil
}
// CurrentPower implements the api.Meter interface
func (c *Connection) CurrentPower() (float64, error) {
s, err := c.statusSnsG.Get()
if err != nil {
return 0, err
}
// SML power available
if sml := s.StatusSNS.SML.PowerCurr; sml != nil {
return *sml, nil
}
var res float64
for _, channel := range c.channels {
power, err := s.StatusSNS.Energy.Power.Value(channel)
if err != nil {
return 0, err
}
res += power
}
return res, nil
}
// TotalEnergy implements the api.MeterEnergy interface
func (c *Connection) TotalEnergy() (float64, error) {
res, err := c.statusSnsG.Get()
if err != nil {
return 0, err
}
// SML total energy available
if sml := res.StatusSNS.SML.TotalIn; sml != nil {
return *sml, err
}
return res.StatusSNS.Energy.Total, err
}
// Powers implements the api.PhasePowers interface
func (c *Connection) Powers() (float64, float64, float64, error) {
s, err := c.statusSnsG.Get()
if err != nil {
return 0, 0, 0, err
}
// SML powers available
if sml := s.StatusSNS.SML; sml.PowerL1 != nil && sml.PowerL2 != nil && sml.PowerL3 != nil {
return *sml.PowerL1, *sml.PowerL2, *sml.PowerL3, nil
}
return c.getPhaseValues(s.StatusSNS.Energy.Power)
}
// Voltages implements the api.PhaseVoltages interface
func (c *Connection) Voltages() (float64, float64, float64, error) {
s, err := c.statusSnsG.Get()
if err != nil {
return 0, 0, 0, err
}
// SML voltages available
if sml := s.StatusSNS.SML; sml.VoltageL1 != nil && sml.VoltageL2 != nil && sml.VoltageL3 != nil {
return *sml.VoltageL1, *sml.VoltageL2, *sml.VoltageL3, nil
}
return c.getPhaseValues(s.StatusSNS.Energy.Voltage)
}
// Currents implements the api.PhaseCurrents interface
func (c *Connection) Currents() (float64, float64, float64, error) {
s, err := c.statusSnsG.Get()
if err != nil {
return 0, 0, 0, err
}
// SML currents available
if sml := s.StatusSNS.SML; sml.CurrentL1 != nil && sml.CurrentL2 != nil && sml.CurrentL3 != nil {
return *sml.CurrentL1, *sml.CurrentL2, *sml.CurrentL3, nil
}
return c.getPhaseValues(s.StatusSNS.Energy.Current)
}
// getPhaseValues returns 3 sequential phase values
func (c *Connection) getPhaseValues(all Channels) (float64, float64, float64, error) {
var res [3]float64
for i, cc := range c.channels {
var err error
res[i], err = all.Value(cc)
if err != nil {
return 0, 0, 0, err
}
}
return res[0], res[1], res[2], nil
}