Add HardyBarth charger with Salia controller (#2894)

This commit is contained in:
andig 2022-03-15 21:00:25 +00:00 • committed by GitHub
parent dedd3ba5c5
commit 1280118473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 550 additions and 62 deletions

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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
}
}

238
charger/hardybarth-salia.go Normal file
View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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

View file

@ -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

View file

@ -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