diff --git a/README.md b/README.md index fec0e4fad4..9db3466062 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G - simple and clean user interface - multiple [chargers](#charger): Wallbe, Phoenix, go-eCharger, NRGKick, SimpleEVSE, EVSEWifi, KEBA/BMW, openWB, Mobile Charger Connect, any other charger using scripting -- multiple [meters](#meter): ModBus (SDM630, MPM3PM, SBC ALE3 and many more), SMA Home Manager and SMA Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC) +- multiple [meters](#meter): ModBus (SDM630, MPM3PM, SBC ALE3 and many more), SMA Home Manager and SMA Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, Tesla PowerWall) - different [vehicles](#vehicle) to show battery status: Audi (eTron), BMW (i3), Tesla, Nissan (Leaf), any other vehicle using scripting - [plugins](#plugins) for integrating with hardware devices and home automation: Modbus (meters and grid inverters), MQTT and shell scripts - status notifications using [Telegram](https://telegram.org) and [PushOver](https://pushover.net) @@ -209,7 +209,7 @@ Meters provide data about power and energy consumption. Available meter implemen energy: Export # optional reading for total energy values, specify for charge meter ``` -- `sma`: SMA Home Manager and SMA Energy Meter. Power reading is configured out of the box but can be customizied if necessary. To obtain energy readings define the desired Obis code (Import Energy: "1:1.8.0", Export Energy: "1:2.8.0"): +- `sma`: SMA Home Manager and SMA Energy Meter. Power reading is configured out of the box but can be customized if necessary. To obtain energy readings define the desired Obis code (Import Energy: "1:1.8.0", Export Energy: "1:2.8.0"): ```yaml - name: sma-home-manager @@ -219,6 +219,17 @@ Meters provide data about power and energy consumption. Available meter implemen energy: # leave empty to disable or choose obis 1:1.8.0/1:2.8.0 ``` +- `tesla`: Tesla PowerWall meter. Use `value` to choose meter (grid meter: `site`, pv: `solar`, battery: `battery`) + + ```yaml + - name: powerwall + type: tesla + uri: http://192.168.1.4/api/meters/aggregates + meter: site # grid meter: `site`, pv: `solar`, battery: `battery` + ``` + + *Note*: this could also be implemented using a `default` meter with the `http` plugin. + - `default`: default meter implementation where meter readings- `power` and `energy` are configured using [plugin](#plugins) ### Vehicle diff --git a/meter/config.go b/meter/config.go index 3215a5e3b9..2337a124cc 100644 --- a/meter/config.go +++ b/meter/config.go @@ -18,6 +18,8 @@ func NewFromConfig(log *util.Logger, typ string, other map[string]interface{}) a c = NewModbusFromConfig(log, other) case "sma": c = NewSMAFromConfig(log, other) + case "tesla", "powerwall": + c = NewTeslaFromConfig(log, other) default: log.FATAL.Fatalf("invalid meter type '%s'", typ) } diff --git a/meter/tesla.go b/meter/tesla.go new file mode 100644 index 0000000000..f6b6241c5a --- /dev/null +++ b/meter/tesla.go @@ -0,0 +1,118 @@ +package meter + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/util" +) + +// credits to https://github.com/vloschiavo/powerwall2 + +type teslaResponse map[string]struct { + LastCommunicationTime string `json:"last_communication_time"` + InstantPower float64 `json:"instant_power"` + InstantReactivePower float64 `json:"instant_reactive_power"` + InstantApparentPower float64 `json:"instant_apparent_power"` + Frequency float64 `json:"frequency"` + EnergyExported float64 `json:"energy_exported"` + EnergyImported float64 `json:"energy_imported"` + InstantAverageVoltage float64 `json:"instant_average_voltage"` + InstantTotalCurrent float64 `json:"instant_total_current"` + IACurrent float64 `json:"i_a_current"` + IBCurrent float64 `json:"i_b_current"` + ICCurrent float64 `json:"i_c_current"` +} + +// Tesla is the tesla powerwall meter +type Tesla struct { + *util.HTTPHelper + uri, usage string +} + +// NewTeslaFromConfig creates a Tesla Powerwall Meter from generic config +func NewTeslaFromConfig(log *util.Logger, other map[string]interface{}) api.Meter { + cc := struct { + URI, Usage string + }{} + util.DecodeOther(log, other, &cc) + + if cc.Usage == "" { + log.FATAL.Fatalf("config: missing usage") + } + + url, err := url.ParseRequestURI(cc.URI) + if err != nil { + log.FATAL.Fatalf("config: invalid uri %s", cc.URI) + } + + if url.Path == "" { + url.Path = "api/meters/aggregates" + cc.URI = url.String() + } + + return NewTesla(cc.URI, cc.Usage) +} + +// NewTesla creates a Tesla Meter +func NewTesla(uri, usage string) api.Meter { + m := &Tesla{ + HTTPHelper: util.NewHTTPHelper(util.NewLogger("tsla")), + uri: uri, + usage: strings.ToLower(usage), + } + + // ignore the self signed certificate + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + m.HTTPHelper.Client.Transport = customTransport + + // decorate api.MeterEnergy + if m.usage == "load" || m.usage == "solar" { + return &TeslaEnergy{Tesla: m} + } + + return m +} + +// CurrentPower implements the Meter.CurrentPower interface +func (m *Tesla) CurrentPower() (float64, error) { + var tr teslaResponse + _, err := m.GetJSON(m.uri, &tr) + + if err == nil { + if o, ok := tr[m.usage]; ok { + return o.InstantPower, nil + } + } + + return 0, fmt.Errorf("invalid usage: %s", m.usage) +} + +// TeslaEnergy decorates Tesla with api.MeterEnergy interface +type TeslaEnergy struct { + *Tesla +} + +// TotalEnergy implements the api.MeterEnergy interface +func (m *TeslaEnergy) TotalEnergy() (float64, error) { + var tr teslaResponse + _, err := m.GetJSON(m.uri, &tr) + + if err == nil { + if o, ok := tr[m.usage]; ok { + if m.usage == "load" { + return o.EnergyImported, nil + } + if m.usage == "solar" { + return o.EnergyExported, nil + } + } + } + + return 0, fmt.Errorf("invalid usage: %s", m.usage) +}