diff --git a/README.md b/README.md index f84fc78f6..467126e44 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G ## Features - simple and clean user interface -- multiple [chargers](#charger): Wallbe, Phoenix (includes ESL Walli), go-eCharger, NRGkick (direct Bluetooth or via Connect device), SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, Fritz!DECT outlets and any other charger using scripting +- multiple [chargers](#charger): Wallbe, Phoenix (includes ESL Walli), go-eCharger, NRGkick (direct Bluetooth or via Connect device), SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, Fritz!DECT outlets, Tasmota outlets and any other charger using scripting - multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall - wide support of vendor-specific [vehicles](#vehicle) interfaces (remote charge, battery and preconditioning status): Audi, BMW, Ford, Hyundai, Kia, Nissan, Porsche, Renault, Seat, Skoda, Tesla, Volkswagen, Volvo and any other connected vehicle using scripting - [plugins](#plugins) for integrating with hardware devices and home automation: Modbus (meters and grid inverters), HTTP, MQTT, Javascript, WebSockets and shell scripts @@ -182,6 +182,7 @@ Charger is responsible for handling EV state and adjusting charge current. Avail - `wallbe`: Wallbe Eco chargers (see [Preparation](#wallbe-preparation-)). For older Wallbe boxes (pre 2019) with Phoenix EV-CC-AC1-M3-CBC-RCM-ETH controllers make sure to set `legacy: true` to enable correct current configuration. - `warp`: Tinkerforge Warp/ Warp Pro charger - `fritzdect`: pseudo charger using Fritz!DECT 200/210 outlets +- `tasmota`: pseudo charger using Tasmota outlets - `default`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger Configuration examples are documented at [andig/evcc-config#chargers](https://github.com/andig/evcc-config#chargers) diff --git a/internal/charger/tasmota.go b/internal/charger/tasmota.go new file mode 100644 index 000000000..bfb9b2356 --- /dev/null +++ b/internal/charger/tasmota.go @@ -0,0 +1,152 @@ +package charger + +import ( + "errors" + "fmt" + "math" + "net/url" + "strings" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/internal/charger/tasmota" + "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" +) + +// Tasmota project homepage +// https://tasmota.github.io/docs/ +// Supported devices: +// https://templates.blakadder.com/ + +// Tasmota charger implementation +type Tasmota struct { + *request.Helper + uri, user, password string + standbypower float64 +} + +func init() { + registry.Add("tasmota", NewTasmotaFromConfig) +} + +// NewTasmotaFromConfig creates a Tasmota charger from generic config +func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + URI string + User string + Password string + StandbyPower float64 + }{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.URI == "" { + return nil, errors.New("missing uri") + } + + return NewTasmota(cc.URI, cc.User, cc.Password, cc.StandbyPower) +} + +// NewTasmota creates Tasmota charger +func NewTasmota(uri, user, password string, standbypower float64) (*Tasmota, error) { + log := util.NewLogger("tasmota") + + c := &Tasmota{ + Helper: request.NewHelper(log), + uri: strings.TrimRight(uri, "/"), + user: user, + password: password, + standbypower: standbypower, + } + + c.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + + return c, nil +} + +// Enabled implements the Charger.Enabled interface +func (c *Tasmota) Enabled() (bool, error) { + var tStatus tasmota.StatusResponse + + // Execute Tasmota Status 0 command + err := c.GetJSON(c.cmdUri("Status 0"), &tStatus) + + return int(1) == tStatus.Status.Power, err +} + +// Enable implements the Charger.Enable interface +func (c *Tasmota) Enable(enable bool) error { + var tPower tasmota.PowerResponse + + cmd := "Power off" + if enable { + cmd = "Power on" + } + + // Execute Tasmota Power on/off command + err := c.GetJSON(c.cmdUri(cmd), &tPower) + + switch { + case err != nil: + return err + case enable && tPower.POWER != "ON": + return errors.New("switchOn failed") + case !enable && tPower.POWER != "OFF": + return errors.New("switchOff failed") + default: + return nil + } +} + +// MaxCurrent implements the Charger.MaxCurrent interface +func (c *Tasmota) MaxCurrent(current int64) error { + return nil +} + +// Status implements the Charger.Status interface +func (c *Tasmota) Status() (api.ChargeStatus, error) { + power, err := c.CurrentPower() + + switch { + case power > 0: + return api.StatusC, err + default: + return api.StatusB, err + } +} + +var _ api.Meter = (*Tasmota)(nil) + +// CurrentPower implements the api.Meter interface +func (c *Tasmota) CurrentPower() (float64, error) { + var tStatusSNS tasmota.StatusSNSResponse + + // Execute Tasmota Status 8 command + err := c.GetJSON(c.cmdUri("Status 8"), &tStatusSNS) + + if err != nil { + return math.NaN(), err + } + power := float64(tStatusSNS.StatusSNS.Energy.Power) + + // ignore standby power + if power < c.standbypower { + power = 0 + } + + return power, err +} + +// cmdUri creates the Tasmota command web request +// https://tasmota.github.io/docs/Commands/#with-web-requests +func (c *Tasmota) cmdUri(cmd string) string { + parameters := url.Values{ + "user": []string{c.user}, + "password": []string{c.password}, + "cmnd": []string{cmd}, + } + + return fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()) +} diff --git a/internal/charger/tasmota/types.go b/internal/charger/tasmota/types.go new file mode 100644 index 000000000..f91ae5a09 --- /dev/null +++ b/internal/charger/tasmota/types.go @@ -0,0 +1,54 @@ +package tasmota + +// StatusResponse is a part of the Tasmota Status 0 command Status response +// https://tasmota.github.io/docs/JSON-Status-Responses/ +type StatusResponse struct { + Status struct { + Module int + DeviceName string + FriendlyName []string + Topic string + ButtonTopic string + Power int + PowerOnState int + LedState int + LedMask string + SaveData int + SaveState int + SwitchTopic string + SwitchMode []int + ButtonRetain int + SwitchRetain int + SensorRetain int + PowerRetain int + InfoRetain int + StateRetain int + } +} + +// StatusSNSResponse is the Tasmota Status 8 command Status response +// https://tasmota.github.io/docs/JSON-Status-Responses/ +type StatusSNSResponse struct { + StatusSNS struct { + Time string + Energy struct { + TotalStartTime string + Total float64 + Yesterday float64 + Today float64 + Power int + ApparentPower int + ReactivePower int + Factor float64 + Voltage int + Current float64 + } + } +} + +// PowerResponse is the Tasmota Power command Status response +// ON, OFF, Error +// https://tasmota.github.io/docs/Commands/#with-web-requests +type PowerResponse struct { + POWER string +}