From 1280118473329fbb186b999fadee00ed28e9f17c Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 15 Mar 2022 21:00:25 +0000 Subject: [PATCH] Add HardyBarth charger with Salia controller (#2894) --- charger/echarge/ecb1/types.go | 32 +++ charger/echarge/salia/types.go | 82 ++++++ charger/echarge/types.go | 30 --- charger/{hardybarth.go => hardybarth-ecb1.go} | 30 ++- charger/hardybarth-salia.go | 238 ++++++++++++++++++ charger/hardybarth-salia_decorators.go | 137 ++++++++++ .../definition/charger/hardybarth-ecb1.yaml | 12 + .../definition/charger/hardybarth-salia.yaml | 12 + templates/definition/charger/hardybarth.yaml | 12 - templates/docs/charger/hardybarth-cph1_0.yaml | 9 - templates/docs/charger/hardybarth-ecb1_0.yaml | 9 + .../docs/charger/hardybarth-salia_0.yaml | 9 + 12 files changed, 550 insertions(+), 62 deletions(-) create mode 100644 charger/echarge/ecb1/types.go create mode 100644 charger/echarge/salia/types.go rename charger/{hardybarth.go => hardybarth-ecb1.go} (85%) create mode 100644 charger/hardybarth-salia.go create mode 100644 charger/hardybarth-salia_decorators.go create mode 100644 templates/definition/charger/hardybarth-ecb1.yaml create mode 100644 templates/definition/charger/hardybarth-salia.yaml delete mode 100644 templates/definition/charger/hardybarth.yaml delete mode 100644 templates/docs/charger/hardybarth-cph1_0.yaml create mode 100644 templates/docs/charger/hardybarth-ecb1_0.yaml create mode 100644 templates/docs/charger/hardybarth-salia_0.yaml diff --git a/charger/echarge/ecb1/types.go b/charger/echarge/ecb1/types.go new file mode 100644 index 000000000..a923b6ba9 --- /dev/null +++ b/charger/echarge/ecb1/types.go @@ -0,0 +1,32 @@ +package ecb1 + +type Meter struct { + ID int + Name string + Data map[string]float64 +} + +type ChargeControl struct { + ID int + Name string + State string + Mode string + ManualModeAmp float64 +} + +type Rfid struct { + ID int + Name string + Data struct { + Tag string + } +} + +type All struct { + ProtocolVersion string `json:"protocol-version"` + Network struct{} + System struct{} + Meters []Meter + ChargeControls []ChargeControl + Rfid []Rfid +} diff --git a/charger/echarge/salia/types.go b/charger/echarge/salia/types.go new file mode 100644 index 000000000..e6f3794bb --- /dev/null +++ b/charger/echarge/salia/types.go @@ -0,0 +1,82 @@ +package salia + +const ( + HeartBeat = "salia/heartbeat" + ChargeMode = "salia/chargemode" + GridCurrentLimit = "grid_current_limit" +) + +type Api struct { + Device struct { + ModelName string + SoftwareVersion string `json:"software_version"` + } + Secc Secc +} + +type Secc struct { + Port0 Port +} + +type Port struct { + Ci struct { + Evse struct { + Basic struct { + OfferedCurrentLimit float64 `json:"offered_current_limit,string"` + } + } + Charge struct { + Cp struct { + Status string + } + } + } + Salia struct { + ChargeMode string + } + Metering struct { + Meter struct { + Type string + Available int `json:",string"` + } + Power struct { + ActiveTotal struct { + Actual float64 `json:",string"` + } `json:"active_total"` + Active struct { + AC struct { + L1 struct { + Actual float64 `json:",string"` + } + L2 struct { + Actual float64 `json:",string"` + } + L3 struct { + Actual float64 `json:",string"` + } + } + } + } + Energy struct { + ActiveImport struct { + Actual float64 `json:",string"` + } `json:"active_import"` + } + Current struct { + AC struct { + L1 struct { + Actual float64 `json:",string"` + } + L2 struct { + Actual float64 `json:",string"` + } + L3 struct { + Actual float64 `json:",string"` + } + } + } + } + EvPresent int `json:"ev_present,string"` + Charging int `json:",string"` + GridCurrentLimit int `json:"grid_current_limit,string"` +} diff --git a/charger/echarge/types.go b/charger/echarge/types.go index d08dfad70..fa5a216d7 100644 --- a/charger/echarge/types.go +++ b/charger/echarge/types.go @@ -4,33 +4,3 @@ const ( ModeEco = "eco" ModeManual = "manual" ) - -type Meter struct { - ID int - Name string - Data map[string]float64 -} - -type ChargeControl struct { - ID int - Name string - State string - Mode string - ManualModeAmp float64 -} - -type Rfid struct { - ID int - Name string - Data struct { - Tag string - } -} - -type All struct { - Network struct{} - System struct{} - Meters []Meter - ChargeControls []ChargeControl - Rfid []Rfid -} diff --git a/charger/hardybarth.go b/charger/hardybarth-ecb1.go similarity index 85% rename from charger/hardybarth.go rename to charger/hardybarth-ecb1.go index 9ade15b06..0dbd4542d 100644 --- a/charger/hardybarth.go +++ b/charger/hardybarth-ecb1.go @@ -18,6 +18,7 @@ package charger // SOFTWARE. import ( + "errors" "fmt" "net/http" "net/url" @@ -25,6 +26,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger/echarge" + "github.com/evcc-io/evcc/charger/echarge/ecb1" "github.com/evcc-io/evcc/meter/obis" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" @@ -44,7 +46,7 @@ type HardyBarth struct { } func init() { - registry.Add("hardybarth", NewHardyBarthFromConfig) + registry.Add("hardybarth-ecb1", NewHardyBarthFromConfig) } // NewHardyBarthFromConfig creates a HardyBarth cPH1 charger from generic config @@ -67,11 +69,13 @@ func NewHardyBarthFromConfig(other map[string]interface{}) (api.Charger, error) // NewHardyBarth creates HardyBarth charger func NewHardyBarth(uri string, chargecontrol, meter int) (api.Charger, error) { - log := util.NewLogger("hardy") + log := util.NewLogger("ecb1") + + uri = strings.TrimSuffix(uri, "/") + "/api/v1" wb := &HardyBarth{ Helper: request.NewHelper(log), - uri: util.DefaultScheme(strings.TrimSuffix(uri, "/"), "http"), + uri: util.DefaultScheme(uri, "http"), chargecontrol: chargecontrol, meter: meter, current: 6, @@ -81,19 +85,19 @@ func NewHardyBarth(uri string, chargecontrol, meter int) (api.Charger, error) { return nil, api.ErrSponsorRequired } - uri = fmt.Sprintf("%s/api/v1/chargecontrols/%d/mode", wb.uri, wb.chargecontrol) + uri = fmt.Sprintf("%s/chargecontrols/%d/mode", wb.uri, wb.chargecontrol) data := url.Values{"mode": {echarge.ModeManual}} err := wb.post(uri, data) return wb, err } -func (wb *HardyBarth) getChargeControl() (echarge.ChargeControl, error) { - uri := fmt.Sprintf("%s/api/v1/chargecontrols/%d", wb.uri, wb.chargecontrol) +func (wb *HardyBarth) getChargeControl() (ecb1.ChargeControl, error) { + uri := fmt.Sprintf("%s/chargecontrols/%d", wb.uri, wb.chargecontrol) var res struct { ChargeControl struct { - echarge.ChargeControl + ecb1.ChargeControl } } @@ -109,6 +113,10 @@ func (wb *HardyBarth) Status() (api.ChargeStatus, error) { return api.StatusNone, err } + if res.State == "" { + return api.StatusNone, errors.New("invalid state- check controller type (eCB1 vs Salia)") + } + switch s := res.State[:1]; s { case "A", "B", "C": return api.ChargeStatus(s), nil @@ -150,7 +158,7 @@ func (wb *HardyBarth) post(uri string, data url.Values) error { } func (wb *HardyBarth) setCurrent(current int64) error { - uri := fmt.Sprintf("%s/api/v1/chargecontrols/%d/mode/manual/ampere", wb.uri, wb.chargecontrol) + uri := fmt.Sprintf("%s/chargecontrols/%d/mode/manual/ampere", wb.uri, wb.chargecontrol) data := url.Values{"manualmodeamp": {fmt.Sprintf("%d", current)}} return wb.post(uri, data) } @@ -164,12 +172,12 @@ func (wb *HardyBarth) MaxCurrent(current int64) error { return err } -func (wb *HardyBarth) getMeter() (echarge.Meter, error) { - uri := fmt.Sprintf("%s/api/v1/meters/%d", wb.uri, wb.meter) +func (wb *HardyBarth) getMeter() (ecb1.Meter, error) { + uri := fmt.Sprintf("%s/meters/%d", wb.uri, wb.meter) var res struct { Meter struct { - echarge.Meter + ecb1.Meter } } diff --git a/charger/hardybarth-salia.go b/charger/hardybarth-salia.go new file mode 100644 index 000000000..fcd57bc23 --- /dev/null +++ b/charger/hardybarth-salia.go @@ -0,0 +1,238 @@ +package charger + +// LICENSE + +// Copyright (c) 2019-2022 andig + +// This module is NOT covered by the MIT license. All rights reserved. + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/charger/echarge" + "github.com/evcc-io/evcc/charger/echarge/salia" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +// http://apidoc.ecb1.de +// https://github.com/evcc-io/evcc/discussions/778 + +// Salia charger implementation +type Salia struct { + *request.Helper + log *util.Logger + uri string + current int64 + res salia.Api + cache time.Duration + updated time.Time +} + +func init() { + registry.Add("hardybarth-salia", NewSaliaFromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decorateSalia -b *Salia -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" + +// NewSaliaFromConfig creates a Salia cPH1 charger from generic config +func NewSaliaFromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + URI string + ChargeControl int + Meter int + Cache time.Duration + }{ + ChargeControl: 1, + Meter: 1, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return NewSalia(cc.URI, cc.Cache) +} + +// NewSalia creates Hardy Barth charger with Salia controller +func NewSalia(uri string, cache time.Duration) (api.Charger, error) { + log := util.NewLogger("salia") + + uri = strings.TrimSuffix(uri, "/") + "/api" + + wb := &Salia{ + log: log, + Helper: request.NewHelper(log), + uri: util.DefaultScheme(uri, "http"), + current: 6, + cache: cache, + } + + // if !sponsor.IsAuthorized() { + // return nil, api.ErrSponsorRequired + // } + + err := wb.post(salia.ChargeMode, echarge.ModeManual) + if err == nil { + go wb.heartbeat() + + res, err := wb.get() + if err == nil && res.Secc.Port0.Metering.Meter.Available > 0 { + return decorateSalia(wb, wb.currentPower, wb.totalEnergy, wb.currents), nil + } + } + + return nil, err +} + +func (wb *Salia) heartbeat() { + _ = wb.post(salia.HeartBeat, "alive") + for range time.NewTicker(30 * time.Second).C { + if err := wb.post(salia.HeartBeat, "alive"); err != nil { + wb.log.ERROR.Println("heartbeat:", err) + } + } +} + +func (wb *Salia) get() (salia.Api, error) { + if time.Since(wb.updated) < wb.cache { + return wb.res, nil + } + + err := wb.GetJSON(wb.uri, &wb.res) + if err == nil { + wb.updated = time.Now() + } + + return wb.res, err +} + +func (wb *Salia) post(key, val string) error { + data := map[string]string{key: val} + uri := fmt.Sprintf("%s/secc", wb.uri) + + req, err := request.New(http.MethodPut, uri, request.MarshalJSON(data), request.JSONEncoding) + if err == nil { + var res struct { + Result string + } + + if err = wb.DoJSON(req, &res); err == nil { + if res.Result != "ok" { + err = fmt.Errorf("invalid result: %s", res.Result) + } + } + } + + return err +} + +// Status implements the api.Charger interface +func (wb *Salia) Status() (api.ChargeStatus, error) { + res, err := wb.get() + if err != nil { + return api.StatusNone, err + } + + switch s := res.Secc.Port0.Ci.Charge.Cp.Status; s { + case "A", "B", "C": + return api.ChargeStatus(s), nil + default: + return api.StatusNone, fmt.Errorf("invalid state: %s", s) + } +} + +// Enabled implements the api.Charger interface +func (wb *Salia) Enabled() (bool, error) { + res, err := wb.get() + if err == nil && res.Secc.Port0.Salia.ChargeMode != echarge.ModeManual { + err = fmt.Errorf("invalid mode: %s", res.Secc.Port0.Salia.ChargeMode) + } + return res.Secc.Port0.GridCurrentLimit > 0, err +} + +// Enable implements the api.Charger interface +func (wb *Salia) Enable(enable bool) error { + var current int64 + if enable { + current = wb.current + } + + err := wb.setCurrent(current) + if err == nil { + wb.updated = time.Time{} + } + + return err +} + +func (wb *Salia) setCurrent(current int64) error { + err := wb.post(salia.GridCurrentLimit, strconv.Itoa(int(current))) + if err == nil { + wb.updated = time.Time{} + } + + return err +} + +// MaxCurrent implements the api.Charger interface +func (wb *Salia) MaxCurrent(current int64) error { + err := wb.setCurrent(current) + if err == nil { + wb.current = current + } + return err +} + +// currentPower implements the api.Meter interface +func (wb *Salia) currentPower() (float64, error) { + res, err := wb.get() + return res.Secc.Port0.Metering.Power.ActiveTotal.Actual / 10, err +} + +// totalEnergy implements the api.MeterEnergy interface +func (wb *Salia) totalEnergy() (float64, error) { + res, err := wb.get() + return res.Secc.Port0.Metering.Energy.ActiveImport.Actual / 1e3, err +} + +// currents implements the api.MeterCurrent interface +func (wb *Salia) currents() (float64, float64, float64, error) { + res, err := wb.get() + i := res.Secc.Port0.Metering.Current.AC + return i.L1.Actual / 1e3, i.L2.Actual / 1e3, i.L3.Actual / 1e3, err +} + +// var _ api.Identifier = (*Salia)(nil) + +// // Identify implements the api.Identifier interface +// func (wb *Salia) Identify() (string, error) { +// return "", api.ErrNotAvailable +// } + +var _ api.Diagnosis = (*Salia)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *Salia) Diagnose() { + res, err := wb.get() + if err == nil { + fmt.Printf("Model name: %s\n", res.Device.ModelName) + fmt.Printf("Software version: %s\n", res.Device.SoftwareVersion) + } +} diff --git a/charger/hardybarth-salia_decorators.go b/charger/hardybarth-salia_decorators.go new file mode 100644 index 000000000..65a045263 --- /dev/null +++ b/charger/hardybarth-salia_decorators.go @@ -0,0 +1,137 @@ +package charger + +// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/evcc-io/evcc/api" +) + +func decorateSalia(base *Salia, meter func() (float64, error), meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error)) api.Charger { + switch { + case meter == nil && meterCurrent == nil && meterEnergy == nil: + return base + + case meter != nil && meterCurrent == nil && meterEnergy == nil: + return &struct { + *Salia + api.Meter + }{ + Salia: base, + Meter: &decorateSaliaMeterImpl{ + meter: meter, + }, + } + + case meter == nil && meterCurrent == nil && meterEnergy != nil: + return &struct { + *Salia + api.MeterEnergy + }{ + Salia: base, + MeterEnergy: &decorateSaliaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meter != nil && meterCurrent == nil && meterEnergy != nil: + return &struct { + *Salia + api.Meter + api.MeterEnergy + }{ + Salia: base, + Meter: &decorateSaliaMeterImpl{ + meter: meter, + }, + MeterEnergy: &decorateSaliaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meter == nil && meterCurrent != nil && meterEnergy == nil: + return &struct { + *Salia + api.MeterCurrent + }{ + Salia: base, + MeterCurrent: &decorateSaliaMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + } + + case meter != nil && meterCurrent != nil && meterEnergy == nil: + return &struct { + *Salia + api.Meter + api.MeterCurrent + }{ + Salia: base, + Meter: &decorateSaliaMeterImpl{ + meter: meter, + }, + MeterCurrent: &decorateSaliaMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + } + + case meter == nil && meterCurrent != nil && meterEnergy != nil: + return &struct { + *Salia + api.MeterCurrent + api.MeterEnergy + }{ + Salia: base, + MeterCurrent: &decorateSaliaMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + MeterEnergy: &decorateSaliaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + + case meter != nil && meterCurrent != nil && meterEnergy != nil: + return &struct { + *Salia + api.Meter + api.MeterCurrent + api.MeterEnergy + }{ + Salia: base, + Meter: &decorateSaliaMeterImpl{ + meter: meter, + }, + MeterCurrent: &decorateSaliaMeterCurrentImpl{ + meterCurrent: meterCurrent, + }, + MeterEnergy: &decorateSaliaMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + } + + return nil +} + +type decorateSaliaMeterImpl struct { + meter func() (float64, error) +} + +func (impl *decorateSaliaMeterImpl) CurrentPower() (float64, error) { + return impl.meter() +} + +type decorateSaliaMeterCurrentImpl struct { + meterCurrent func() (float64, float64, float64, error) +} + +func (impl *decorateSaliaMeterCurrentImpl) Currents() (float64, float64, float64, error) { + return impl.meterCurrent() +} + +type decorateSaliaMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateSaliaMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} diff --git a/templates/definition/charger/hardybarth-ecb1.yaml b/templates/definition/charger/hardybarth-ecb1.yaml new file mode 100644 index 000000000..cc040e369 --- /dev/null +++ b/templates/definition/charger/hardybarth-ecb1.yaml @@ -0,0 +1,12 @@ +template: hardybarth-ecb1 +products: +- brand: hardybarth-ecb1 + description: + generic: HardyBarth cPH1 (eCB1 controller) +params: +- name: host + required: true + example: 192.0.2.2 +render: | + type: hardybarth-ecb1 + uri: http://{{ .host }} diff --git a/templates/definition/charger/hardybarth-salia.yaml b/templates/definition/charger/hardybarth-salia.yaml new file mode 100644 index 000000000..da110f53e --- /dev/null +++ b/templates/definition/charger/hardybarth-salia.yaml @@ -0,0 +1,12 @@ +template: hardybarth-salia +products: +- brand: hardybarth-salia + description: + generic: HardyBarth (Salia PLCC controller) +params: +- name: host + required: true + example: 192.0.2.2 +render: | + type: hardybarth-salia + uri: http://{{ .host }} diff --git a/templates/definition/charger/hardybarth.yaml b/templates/definition/charger/hardybarth.yaml deleted file mode 100644 index 1cde1353a..000000000 --- a/templates/definition/charger/hardybarth.yaml +++ /dev/null @@ -1,12 +0,0 @@ -template: hardybarth-cph1 -products: -- brand: hardybarth-cph1 - description: - generic: HardyBarth cPH1 -params: -- name: host - required: true - example: 192.0.2.2 -render: | - type: hardybarth - uri: http://{{ .host }} diff --git a/templates/docs/charger/hardybarth-cph1_0.yaml b/templates/docs/charger/hardybarth-cph1_0.yaml deleted file mode 100644 index 60a28b22e..000000000 --- a/templates/docs/charger/hardybarth-cph1_0.yaml +++ /dev/null @@ -1,9 +0,0 @@ -product: - brand: hardybarth-cph1 - description: HardyBarth cPH1 - -render: -- default: | - type: template - template: hardybarth-cph1 - host: 192.0.2.2 # IP-Adresse oder Hostname \ No newline at end of file diff --git a/templates/docs/charger/hardybarth-ecb1_0.yaml b/templates/docs/charger/hardybarth-ecb1_0.yaml new file mode 100644 index 000000000..bff54b3ac --- /dev/null +++ b/templates/docs/charger/hardybarth-ecb1_0.yaml @@ -0,0 +1,9 @@ +product: + brand: hardybarth-ecb1 + description: HardyBarth cPH1 (eCB1 controller) + +render: +- default: | + type: template + template: hardybarth-ecb1 + host: 192.0.2.2 # IP-Adresse oder Hostname \ No newline at end of file diff --git a/templates/docs/charger/hardybarth-salia_0.yaml b/templates/docs/charger/hardybarth-salia_0.yaml new file mode 100644 index 000000000..cf88f0611 --- /dev/null +++ b/templates/docs/charger/hardybarth-salia_0.yaml @@ -0,0 +1,9 @@ +product: + brand: hardybarth-salia + description: HardyBarth (Salia PLCC controller) + +render: +- default: | + type: template + template: hardybarth-salia + host: 192.0.2.2 # IP-Adresse oder Hostname \ No newline at end of file