Tasmota: enable meter/relay channel-list (#10171)

This commit is contained in:
Markus Thierolf 2023-10-31 21:09:21 +01:00 • committed by GitHub
parent d212f04b28
commit 8d8ac5c72e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 305 additions and 106 deletions

View file

@ -15,8 +15,7 @@ import (
// Tasmota charger implementation
type Tasmota struct {
conn *tasmota.Connection
channel int
conn *tasmota.Connection
*switchSocket
}
@ -32,10 +31,10 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
User string
Password string
StandbyPower float64
Channel int
Channel []int
Cache time.Duration
}{
Channel: 1,
Channel: []int{1},
Cache: time.Second,
}
@ -47,20 +46,19 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
}
// NewTasmota creates Tasmota charger
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)
func NewTasmota(embed embed, uri, user, password string, channels []int, standbypower float64, cache time.Duration) (*Tasmota, error) {
conn, err := tasmota.NewConnection(uri, user, password, channels, cache)
if err != nil {
return nil, err
}
c := &Tasmota{
conn: conn,
channel: channel,
conn: conn,
}
c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
return c, c.conn.ChannelExists(channel)
return c, c.conn.RelayExists()
}
// Enabled implements the api.Charger interface
@ -79,3 +77,17 @@ var _ api.MeterEnergy = (*Tasmota)(nil)
func (c *Tasmota) TotalEnergy() (float64, error) {
return c.conn.TotalEnergy()
}
var _ api.PhaseCurrents = (*Tasmota)(nil)
// Currents implements the api.PhaseCurrents interface
func (c *Tasmota) Currents() (float64, float64, float64, error) {
return c.conn.Currents()
}
var _ api.PhaseVoltages = (*Tasmota)(nil)
// Voltages implements the api.PhaseVoltages interface
func (c *Tasmota) Voltages() (float64, float64, float64, error) {
return c.conn.Voltages()
}

View file

@ -1,6 +1,7 @@
package meter
import (
"strings"
"time"
"github.com/evcc-io/evcc/api"
@ -19,17 +20,19 @@ func init() {
registry.Add("tasmota", NewTasmotaFromConfig)
}
//go:generate go run ../cmd/tools/decorate.go -f decorateTasmota -b *Tasmota -r api.Meter -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)"
// NewTasmotaFromConfig creates a Tasmota meter from generic config
func NewTasmotaFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI string
User string
Password string
Channel int
Channel []int
Usage string
Cache time.Duration
}{
Channel: 1,
Channel: []int{1},
Cache: time.Second,
}
@ -37,12 +40,12 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Meter, error) {
return nil, err
}
return NewTasmota(cc.URI, cc.User, cc.Password, cc.Usage, cc.Channel, cc.Cache)
return NewTasmota(cc.URI, cc.User, cc.Password, strings.ToLower(cc.Usage), cc.Channel, cc.Cache)
}
// NewTasmota creates Tasmota meter
func NewTasmota(uri, user, password, usage string, channel int, cache time.Duration) (*Tasmota, error) {
conn, err := tasmota.NewConnection(uri, user, password, channel, cache)
func NewTasmota(uri, user, password, usage string, channels []int, cache time.Duration) (api.Meter, error) {
conn, err := tasmota.NewConnection(uri, user, password, channels, cache)
if err != nil {
return nil, err
}
@ -52,7 +55,13 @@ func NewTasmota(uri, user, password, usage string, channel int, cache time.Durat
usage: usage,
}
return c, err
var currents, voltages func() (float64, float64, float64, error)
if usage != "grid" {
currents = c.currents
voltages = c.voltages
}
return decorateTasmota(c, currents, voltages), nil
}
var _ api.Meter = (*Tasmota)(nil)
@ -74,3 +83,13 @@ func (c *Tasmota) TotalEnergy() (float64, error) {
}
return c.conn.TotalEnergy()
}
// currents implements the api.PhaseCurrents interface
func (c *Tasmota) currents() (float64, float64, float64, error) {
return c.conn.Currents()
}
// voltages implements the api.PhaseVoltages interface
func (c *Tasmota) voltages() (float64, float64, float64, error) {
return c.conn.Voltages()
}

View file

@ -17,24 +17,35 @@ import (
type Connection struct {
*request.Helper
uri, user, password string
channel int
channels []int
statusSnsG provider.Cacheable[StatusSNSResponse]
statusStsG provider.Cacheable[StatusSTSResponse]
}
// NewConnection creates a Tasmota connection
func NewConnection(uri, user, password string, channel int, cache time.Duration) (*Connection, error) {
func NewConnection(uri, user, password string, channels []int, cache time.Duration) (*Connection, error) {
if uri == "" {
return nil, errors.New("missing uri")
}
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,
channel: channel,
channels: channels,
}
c.Client.Transport = request.NewTripper(log, transport.Insecure())
@ -65,34 +76,36 @@ func NewConnection(uri, user, password string, channel int, cache time.Duration)
}
// channelExists checks the existence of the configured relay channel interface
func (c *Connection) ChannelExists(channel int) error {
func (c *Connection) RelayExists() error {
res, err := c.statusStsG.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 != ""
}
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)
if !ok {
return fmt.Errorf("invalid relay channel: %d", channel)
}
}
return nil
@ -100,53 +113,56 @@ func (c *Connection) ChannelExists(channel int) error {
// 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)
}
for _, channel := range c.channels {
parameters := url.Values{
"user": []string{c.user},
"password": []string{c.password},
"cmnd": []string{cmd},
}
cmd := fmt.Sprintf("Power%d off", channel)
if enable {
cmd = fmt.Sprintf("Power%d on", channel)
}
var res PowerResponse
if err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res); err != nil {
return err
}
parameters := url.Values{
"user": []string{c.user},
"password": []string{c.password},
"cmnd": []string{cmd},
}
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"
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()
switch {
case enable && !on:
return errors.New("switchOn failed")
case !enable && on:
return errors.New("switchOff failed")
default:
return nil
}
return nil
}
// Enabled implements the api.Charger interface
@ -156,33 +172,46 @@ func (c *Connection) Enabled() (bool, error) {
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
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) {
res, err := c.statusSnsG.Get()
s, err := c.statusSnsG.Get()
if err != nil {
return 0, err
}
return res.StatusSNS.Energy.Power.Channel(c.channel)
var res float64
for _, channel := range c.channels {
power, err := s.StatusSNS.Energy.Power.Channel(channel)
if err != nil {
return 0, err
}
res += power
}
return res, nil
}
// TotalEnergy implements the api.MeterEnergy interface
@ -191,6 +220,42 @@ func (c *Connection) TotalEnergy() (float64, error) {
return res.StatusSNS.Energy.Total, err
}
// 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
}
var res [3]float64
for i, c := range c.channels {
res[i], err = s.StatusSNS.Energy.Current.Channel(c)
if err != nil {
return 0, 0, 0, err
}
}
return res[0], res[1], res[2], err
}
// 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
}
var res [3]float64
for i, c := range c.channels {
res[i], err = s.StatusSNS.Energy.Voltage.Channel(c)
if err != nil {
return 0, 0, 0, err
}
}
return res[0], res[1], res[2], err
}
// SmlPower provides the sml sensor power
func (c *Connection) SmlPower() (float64, error) {
res, err := c.statusSnsG.Get()

View file

@ -0,0 +1,69 @@
package meter
// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
import (
"github.com/evcc-io/evcc/api"
)
func decorateTasmota(base *Tasmota, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error)) api.Meter {
switch {
case phaseCurrents == nil && phaseVoltages == nil:
return base
case phaseCurrents == nil && phaseVoltages != nil:
return &struct {
*Tasmota
api.PhaseVoltages
}{
Tasmota: base,
PhaseVoltages: &decorateTasmotaPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
case phaseCurrents != nil && phaseVoltages == nil:
return &struct {
*Tasmota
api.PhaseCurrents
}{
Tasmota: base,
PhaseCurrents: &decorateTasmotaPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
}
case phaseCurrents != nil && phaseVoltages != nil:
return &struct {
*Tasmota
api.PhaseCurrents
api.PhaseVoltages
}{
Tasmota: base,
PhaseCurrents: &decorateTasmotaPhaseCurrentsImpl{
phaseCurrents: phaseCurrents,
},
PhaseVoltages: &decorateTasmotaPhaseVoltagesImpl{
phaseVoltages: phaseVoltages,
},
}
}
return nil
}
type decorateTasmotaPhaseCurrentsImpl struct {
phaseCurrents func() (float64, float64, float64, error)
}
func (impl *decorateTasmotaPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
return impl.phaseCurrents()
}
type decorateTasmotaPhaseVoltagesImpl struct {
phaseVoltages func() (float64, float64, float64, error)
}
func (impl *decorateTasmotaPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
return impl.phaseVoltages()
}

View file

@ -1,6 +1,6 @@
template: tasmota
products:
- brand: Tasmota
- brand: Tasmota (1 Phase Charger)
group: switchsockets
params:
- name: host
@ -16,11 +16,11 @@ params:
default: 1
required: true
description:
de: Nummer des Schaltkanals
en: Device relay channel number
de: Schaltkanal Nummer
en: Relaychannel number
help:
de: Nummer des Schaltkanals (1-8), bei Geräten mit mehr als einem Schalter
en: Device relay channel number (allowed range 1-8)
de: Schaltkanal (1-8)
en: Relaychannel number (1-8)
- preset: switchsocket
render: |
type: tasmota
@ -31,5 +31,5 @@ render: |
{{- if .password }}
password: {{ .password }}
{{- end }}
channel: {{ .channel }} # relay channel (allowed range: 1-8)
channel: [{{ .channel }}] # list of relay channels [1,2,....,8]
{{ include "switchsocket" . }}

View file

@ -0,0 +1,24 @@
template: tasmota-3p
products:
- description:
generic: Tasmota (3 Phase Meter - meter channels 1,2+3 must be used)
group: switchsockets
params:
- name: usage
choice: ["grid", "pv", "battery", "charge"]
- name: host
- name: user
required: false
help:
de: Standard-User ist admin
en: admin is default
- name: password
required: false
mask: true
render: |
type: tasmota
uri: http://{{ .host }}
usage: {{ .usage }}
user: {{ .user }}
password: {{ .password }}
channel: [1,2,3] # list of meter channels [1,2,....,8]

View file

@ -1,11 +1,11 @@
template: tasmota
products:
- description:
generic: Tasmota
generic: Tasmota (1 Phase Meter)
group: switchsockets
params:
- name: usage
choice: ["grid", "pv", "battery"]
choice: ["grid", "pv", "battery", "charge"]
- name: host
- name: user
required: false
@ -15,9 +15,19 @@ params:
- name: password
required: false
mask: true
- name: channel
default: 1
required: true
description:
de: Kanal Nummer
en: Channel number
help:
de: Meterkanal (1-8)
en: Meterchannel number (1-8)
render: |
type: tasmota
uri: http://{{ .host }}
usage: {{ .usage }}
user: {{ .user }}
password: {{ .password }}
channel: [{{ .channel }}] # list of meter channels [1,2,....,8]

View file

@ -78,7 +78,7 @@
"HomeWizard",
"myStrom",
"Shelly",
"Tasmota",
"Tasmota (1 Phase Charger)",
"TP-Link"
],
"Meters": [