From 517916bf385e5e0f95d017ca4ca0d3256179a9b6 Mon Sep 17 00:00:00 2001 From: andig Date: Tue, 30 Sep 2025 10:57:50 +0200 Subject: [PATCH] Daheimladen: remove deprecated template (BC) (#23946) --- charger/daheimladen-mb.go | 359 -------------- charger/daheimladen.go | 442 ++++++++++++------ ...ecorators.go => daheimladen_decorators.go} | 10 +- .../definition/charger/daheimladen-mb.yaml | 17 - .../definition/charger/daheimladen-pro.yaml | 2 +- templates/definition/charger/daheimladen.yaml | 23 +- 6 files changed, 310 insertions(+), 543 deletions(-) delete mode 100644 charger/daheimladen-mb.go rename charger/{daheimladen-mb_decorators.go => daheimladen_decorators.go} (84%) delete mode 100644 templates/definition/charger/daheimladen-mb.yaml diff --git a/charger/daheimladen-mb.go b/charger/daheimladen-mb.go deleted file mode 100644 index 9a92d4d9f..000000000 --- a/charger/daheimladen-mb.go +++ /dev/null @@ -1,359 +0,0 @@ -package charger - -// LICENSE - -// Copyright (c) evcc.io (andig, naltatis, premultiply) - -// 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 ( - "context" - "encoding/binary" - "fmt" - "time" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/modbus" -) - -// DaheimLadenMB charger implementation -type DaheimLadenMB struct { - log *util.Logger - conn *modbus.Connection - curr uint16 - phases uint16 -} - -const ( - dlRegChargingState = 0 // Uint16 RO ENUM - dlRegConnectorState = 2 // Uint16 RO ENUM - dlRegCurrents = 6 // 3xUint16 plus placeholder RO 0.1A - dlRegActivePower = 12 // Uint32 RO 1W - dlRegTotalEnergy = 28 // Uint32 RO 0.1KWh - dlRegEvseMaxCurrent = 32 // Uint16 RO 0.1A - dlRegCableMaxCurrent = 36 // Uint16 RO 0.1A - dlRegStationId = 38 // Chr[16] RO UTF16 - dlRegChargedEnergy = 72 // Uint16 RO 0.1kWh - dlRegChargingTime = 78 // Uint32 RO 1s - dlRegSafeCurrent = 87 // Uint16 WR 0.1A - dlRegCommTimeout = 89 // Uint16 WR 1s - dlRegCurrentLimit = 91 // Uint16 WR 0.1A - dlRegChargeControl = 93 // Uint16 WR ENUM - dlRegChargeCmd = 95 // Uint16 WR ENUM - dlRegVoltages = 109 // 3xUint16 plus placeholder RO 0.1V - - // PRO only - dlRegPhaseSwitchState = 184 - dlRegPhaseSwitchControl = 186 - dlRegPhaseSwitchAction = 188 -) - -func init() { - registry.AddCtx("daheimladen-mb", NewDaheimLadenMBFromConfig) -} - -//go:generate go tool decorate -f decorateDaheimLaden -b *DaheimLadenMB -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)" - -// NewDaheimLadenMBFromConfig creates a DaheimLadenMB charger from generic config -func NewDaheimLadenMBFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { - cc := struct { - modbus.TcpSettings `mapstructure:",squash"` - Phases1p3p bool - }{ - TcpSettings: modbus.TcpSettings{ - ID: 255, - }, - } - - if err := util.DecodeOther(other, &cc); err != nil { - return nil, err - } - - return NewDaheimLadenMB(ctx, cc.URI, cc.ID, cc.Phases1p3p) -} - -// NewDaheimLadenMB creates DaheimLadenMB charger -func NewDaheimLadenMB(ctx context.Context, uri string, id uint8, phases bool) (api.Charger, error) { - conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id) - if err != nil { - return nil, err - } - - log := util.NewLogger("daheimladen-mb") - conn.Logger(log.TRACE) - - wb := &DaheimLadenMB{ - log: log, - conn: conn, - curr: 60, // assume min current - phases: 3, // assume 3p - } - - // get initial state from charger - curr, err := wb.getCurrent() - if err != nil { - return nil, fmt.Errorf("current limit: %w", err) - } - if curr > 0 { - wb.curr = curr - } - - // get failsafe timeout from charger - b, err := wb.conn.ReadHoldingRegisters(dlRegCommTimeout, 1) - if err != nil { - return nil, fmt.Errorf("failsafe timeout: %w", err) - } - if u := binary.BigEndian.Uint16(b); u > 0 { - go wb.heartbeat(ctx, time.Duration(u)*time.Second/2) - } - - var phases1p3p func(int) error - var phasesG func() (int, error) - if phases { - phases1p3p = wb.phases1p3p - phasesG = wb.getPhases - } - - return decorateDaheimLaden(wb, phases1p3p, phasesG), nil -} - -func (wb *DaheimLadenMB) heartbeat(ctx context.Context, timeout time.Duration) { - for tick := time.Tick(timeout); ; { - select { - case <-tick: - case <-ctx.Done(): - return - } - - if _, err := wb.conn.ReadHoldingRegisters(dlRegSafeCurrent, 1); err != nil { - wb.log.ERROR.Println("heartbeat:", err) - } - } -} - -func (wb *DaheimLadenMB) setCurrent(current uint16) error { - b := make([]byte, 2) - binary.BigEndian.PutUint16(b, current) - - _, err := wb.conn.WriteMultipleRegisters(dlRegCurrentLimit, 1, b) - - return err -} - -func (wb *DaheimLadenMB) getCurrent() (uint16, error) { - b, err := wb.conn.ReadHoldingRegisters(dlRegCurrentLimit, 1) - if err != nil { - return 0, err - } - - return binary.BigEndian.Uint16(b), nil -} - -// Status implements the api.Charger interface -func (wb *DaheimLadenMB) Status() (api.ChargeStatus, error) { - b, err := wb.conn.ReadHoldingRegisters(dlRegChargingState, 1) - if err != nil { - return api.StatusNone, err - } - - s := binary.BigEndian.Uint16(b) - - switch s { - case 1: // Standby (A) - return api.StatusA, nil - case 2: // Connect (B1) - return api.StatusB, nil - case 3: // Start-up State (B2) - return api.StatusB, nil - case 4: // Charging (C) - power, err := wb.CurrentPower() - if power == 0 { - return api.StatusB, err - } - return api.StatusC, err - case 5: // Start-UP Fail (B2) - return api.StatusB, nil - case 6: // Session Terminated by EVSE - return api.StatusB, nil - default: // Other - return api.StatusNone, fmt.Errorf("invalid status: %d", s) - } -} - -// Enabled implements the api.Charger interface -func (wb *DaheimLadenMB) Enabled() (bool, error) { - curr, err := wb.getCurrent() - - return curr > 0, err -} - -// Enable implements the api.Charger interface -func (wb *DaheimLadenMB) Enable(enable bool) error { - var current uint16 - if enable { - current = wb.curr - } - - // break to avoid too fast commands - time.Sleep(time.Second) - - return wb.setCurrent(current) -} - -// MaxCurrent implements the api.Charger interface -func (wb *DaheimLadenMB) MaxCurrent(current int64) error { - return wb.MaxCurrentMillis(float64(current)) -} - -var _ api.ChargerEx = (*DaheimLadenMB)(nil) - -// MaxCurrentMillis implements the api.ChargerEx interface -func (wb *DaheimLadenMB) MaxCurrentMillis(current float64) error { - if current < 6 { - return fmt.Errorf("invalid current %.1f", current) - } - - curr := uint16(current * 10) - - err := wb.setCurrent(curr) - if err == nil { - wb.curr = curr - } - - return err -} - -var _ api.Meter = (*DaheimLadenMB)(nil) - -// CurrentPower implements the api.Meter interface -func (wb *DaheimLadenMB) CurrentPower() (float64, error) { - b, err := wb.conn.ReadHoldingRegisters(dlRegActivePower, 2) - if err != nil { - return 0, err - } - - return float64(binary.BigEndian.Uint32(b)), err -} - -var _ api.MeterEnergy = (*DaheimLadenMB)(nil) - -// TotalEnergy implements the api.MeterEnergy interface -func (wb *DaheimLadenMB) TotalEnergy() (float64, error) { - b, err := wb.conn.ReadHoldingRegisters(dlRegTotalEnergy, 2) - if err != nil { - return 0, err - } - - return float64(binary.BigEndian.Uint32(b)) / 10, err -} - -// getPhaseValues returns 3 sequential register values -func (wb *DaheimLadenMB) getPhaseValues(reg uint16) (float64, float64, float64, error) { - b, err := wb.conn.ReadHoldingRegisters(reg, 5) - if err != nil { - return 0, 0, 0, err - } - - var res [3]float64 - for i := range res { - // 16-bit registers for currents/voltages spaced by 1 empty register - res[i] = float64(binary.BigEndian.Uint16(b[4*i:])) / 10 - } - - return res[0], res[1], res[2], nil -} - -var _ api.PhaseCurrents = (*DaheimLadenMB)(nil) - -// Currents implements the api.PhaseCurrents interface -func (wb *DaheimLadenMB) Currents() (float64, float64, float64, error) { - return wb.getPhaseValues(dlRegCurrents) -} - -var _ api.PhaseVoltages = (*DaheimLadenMB)(nil) - -// Voltages implements the api.PhaseVoltages interface -func (wb *DaheimLadenMB) Voltages() (float64, float64, float64, error) { - return wb.getPhaseValues(dlRegVoltages) -} - -// phases1p3p implements the api.PhaseSwitcher interface -func (wb *DaheimLadenMB) phases1p3p(phases int) error { - b := make([]byte, 2) - binary.BigEndian.PutUint16(b, uint16(phases)) - - _, err := wb.conn.WriteMultipleRegisters(dlRegPhaseSwitchControl, 1, b) - if err == nil { - wb.phases = uint16(phases) - } - return err -} - -// getPhases implements the api.PhaseGetter interface -func (wb *DaheimLadenMB) getPhases() (int, error) { - b, err := wb.conn.ReadHoldingRegisters(dlRegPhaseSwitchAction, 1) - if err != nil { - return 0, err - } - - // 0=standby, 1=changing, 2=changed - if binary.BigEndian.Uint16(b) != 1 { - b, err := wb.conn.ReadHoldingRegisters(dlRegPhaseSwitchState, 1) - if err != nil { - return 0, err - } - - wb.phases = binary.BigEndian.Uint16(b) - } - - return int(wb.phases), nil -} - -var _ api.Diagnosis = (*DaheimLadenMB)(nil) - -// Diagnose implements the api.Diagnosis interface -func (wb *DaheimLadenMB) Diagnose() { - if b, err := wb.conn.ReadHoldingRegisters(dlRegChargingState, 1); err == nil { - fmt.Printf("\tCharging Station State:\t%d\n", binary.BigEndian.Uint16(b)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegConnectorState, 1); err == nil { - fmt.Printf("\tConnector State:\t%d\n", binary.BigEndian.Uint16(b)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegEvseMaxCurrent, 1); err == nil { - fmt.Printf("\tEVSE Max. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegCableMaxCurrent, 1); err == nil { - fmt.Printf("\tCable Max. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegStationId, 16); err == nil { - s, _ := utf16BEBytesAsString(b) - fmt.Printf("\tStation ID:\t%s\n", s) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegSafeCurrent, 1); err == nil { - fmt.Printf("\tSafe Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegCommTimeout, 1); err == nil { - fmt.Printf("\tConnection Timeout:\t%d\n", binary.BigEndian.Uint16(b)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegCurrentLimit, 1); err == nil { - fmt.Printf("\tCurrent Limit:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegChargeControl, 1); err == nil { - fmt.Printf("\tCharge Control:\t%d\n", binary.BigEndian.Uint16(b)) - } - if b, err := wb.conn.ReadHoldingRegisters(dlRegChargeCmd, 1); err == nil { - fmt.Printf("\tCharge Command:\t%d\n", binary.BigEndian.Uint16(b)) - } -} diff --git a/charger/daheimladen.go b/charger/daheimladen.go index c409165cf..3773ecef0 100644 --- a/charger/daheimladen.go +++ b/charger/daheimladen.go @@ -1,186 +1,235 @@ package charger +// LICENSE + +// Copyright (c) evcc.io (andig, naltatis, premultiply) + +// 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 ( + "context" + "encoding/binary" "fmt" - "net/http" - "strconv" "time" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/charger/daheimladen" "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/request" - "golang.org/x/oauth2" + "github.com/evcc-io/evcc/util/modbus" ) // DaheimLaden charger implementation type DaheimLaden struct { - *request.Helper - stationID string - connectorID int32 - idTag string - token string - transactionID int32 - statusG util.Cacheable[daheimladen.GetLatestStatus] - meterG util.Cacheable[daheimladen.GetLatestMeterValueResponse] - cache time.Duration + log *util.Logger + conn *modbus.Connection + curr uint16 + phases uint16 } +const ( + dlRegChargingState = 0 // Uint16 RO ENUM + dlRegConnectorState = 2 // Uint16 RO ENUM + dlRegCurrents = 6 // 3xUint16 plus placeholder RO 0.1A + dlRegActivePower = 12 // Uint32 RO 1W + dlRegTotalEnergy = 28 // Uint32 RO 0.1KWh + dlRegEvseMaxCurrent = 32 // Uint16 RO 0.1A + dlRegCableMaxCurrent = 36 // Uint16 RO 0.1A + dlRegStationId = 38 // Chr[16] RO UTF16 + dlRegChargedEnergy = 72 // Uint16 RO 0.1kWh + dlRegChargingTime = 78 // Uint32 RO 1s + dlRegSafeCurrent = 87 // Uint16 WR 0.1A + dlRegCommTimeout = 89 // Uint16 WR 1s + dlRegCurrentLimit = 91 // Uint16 WR 0.1A + dlRegChargeControl = 93 // Uint16 WR ENUM + dlRegChargeCmd = 95 // Uint16 WR ENUM + dlRegVoltages = 109 // 3xUint16 plus placeholder RO 0.1V + + // PRO only + dlRegPhaseSwitchState = 184 + dlRegPhaseSwitchControl = 186 + dlRegPhaseSwitchAction = 188 +) + func init() { - registry.Add("daheimladen", NewDaheimLadenFromConfig) + registry.AddCtx("daheimladen", NewDaheimLadenFromConfig) } +//go:generate go tool decorate -f decorateDaheimLaden -b *DaheimLaden -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)" + // NewDaheimLadenFromConfig creates a DaheimLaden charger from generic config -func NewDaheimLadenFromConfig(other map[string]interface{}) (api.Charger, error) { +func NewDaheimLadenFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { cc := struct { - Token string - StationID string - Cache time.Duration + modbus.TcpSettings `mapstructure:",squash"` + Phases1p3p bool }{ - Cache: time.Second, + TcpSettings: modbus.TcpSettings{ + ID: 255, + }, } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } - return NewDaheimLaden(cc.Token, cc.StationID, cc.Cache) + return NewDaheimLaden(ctx, cc.URI, cc.ID, cc.Phases1p3p) } // NewDaheimLaden creates DaheimLaden charger -func NewDaheimLaden(token, stationID string, cache time.Duration) (*DaheimLaden, error) { - c := &DaheimLaden{ - Helper: request.NewHelper(util.NewLogger("daheim")), - stationID: stationID, - connectorID: 1, - idTag: daheimladen.EVCC_IDTAG, - token: token, - cache: cache, - } - - c.Client.Transport = &oauth2.Transport{ - Source: oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: token, - TokenType: "Bearer", - }), - Base: c.Client.Transport, - } - - c.statusG = util.ResettableCached(func() (daheimladen.GetLatestStatus, error) { - var res daheimladen.GetLatestStatus - err := c.GetJSON(fmt.Sprintf("%s/cs/%s/status", daheimladen.BASE_URL, c.stationID), &res) - return res, err - }, c.cache) - - c.meterG = util.ResettableCached(func() (daheimladen.GetLatestMeterValueResponse, error) { - var res daheimladen.GetLatestMeterValueResponse - err := c.GetJSON(fmt.Sprintf("%s/cs/%s/metervalue", daheimladen.BASE_URL, c.stationID), &res) - return res, err - }, c.cache) - - return c, nil -} - -// reset cache -func (c *DaheimLaden) reset() { - c.statusG.Reset() - c.meterG.Reset() -} - -// Status implements the api.Charger interface -func (c *DaheimLaden) Status() (api.ChargeStatus, error) { - res, err := c.statusG.Get() +func NewDaheimLaden(ctx context.Context, uri string, id uint8, phases bool) (api.Charger, error) { + conn, err := modbus.NewConnection(ctx, uri, "", "", 0, modbus.Tcp, id) if err != nil { - return api.StatusNone, err + return nil, err } - status := daheimladen.ChargePointStatus(res.Status) - switch status { - case daheimladen.AVAILABLE: - return api.StatusA, nil - case daheimladen.PREPARING: - return api.StatusB, nil - case daheimladen.CHARGING, daheimladen.FINISHING: - return api.StatusC, nil - default: - return api.StatusNone, fmt.Errorf("invalid status: %s", res.Status) - } -} + log := util.NewLogger("daheimladen") + conn.Logger(log.TRACE) -// Enabled implements the api.Charger interface -func (c *DaheimLaden) Enabled() (bool, error) { - res, err := c.statusG.Get() - return res.Status == string(daheimladen.CHARGING), err -} - -// Enable implements the api.Charger interface -func (c *DaheimLaden) Enable(enable bool) error { - defer c.reset() - - if enable { - data := daheimladen.RemoteStartRequest{ - ConnectorID: c.connectorID, - IdTag: c.idTag, - } - - uri := fmt.Sprintf("%s/cs/%s/remotestart", daheimladen.BASE_URL, c.stationID) - req, err := http.NewRequest(http.MethodPost, uri, request.MarshalJSON(data)) - if err != nil { - return err - } - - var res daheimladen.RemoteStartResponse - if err = c.DoJSON(req, &res); err == nil && res.Status != string(daheimladen.REMOTE_START_ACCEPTED) { - err = fmt.Errorf("charging station refused to start transaction") - } - - return err + wb := &DaheimLaden{ + log: log, + conn: conn, + curr: 60, // assume min current + phases: 3, // assume 3p } - var res daheimladen.GetLatestInProgressTransactionResponse - uri := fmt.Sprintf("%s/cs/%s/get_latest_inprogress_transaction", daheimladen.BASE_URL, c.stationID) - if err := c.GetJSON(uri, &res); err != nil { - return err - } - - c.transactionID = res.TransactionID - - data := daheimladen.RemoteStopRequest{ - TransactionID: c.transactionID, - } - - uri = fmt.Sprintf("%s/cs/%s/remotestop", daheimladen.BASE_URL, c.stationID) - req, err := http.NewRequest(http.MethodPost, uri, request.MarshalJSON(data)) + // get initial state from charger + curr, err := wb.getCurrent() if err != nil { - return err + return nil, fmt.Errorf("current limit: %w", err) + } + if curr > 0 { + wb.curr = curr } - var remoteStopRes daheimladen.RemoteStartResponse - if err = c.DoJSON(req, &remoteStopRes); err == nil && remoteStopRes.Status != string(daheimladen.REMOTE_STOP_ACCEPTED) { - err = fmt.Errorf("charging station refused to stop transaction") + // get failsafe timeout from charger + b, err := wb.conn.ReadHoldingRegisters(dlRegCommTimeout, 1) + if err != nil { + return nil, fmt.Errorf("failsafe timeout: %w", err) } + if u := binary.BigEndian.Uint16(b); u > 0 { + go wb.heartbeat(ctx, time.Duration(u)*time.Second/2) + } + + var phases1p3p func(int) error + var phasesG func() (int, error) + if phases { + phases1p3p = wb.phases1p3p + phasesG = wb.getPhases + } + + return decorateDaheimLaden(wb, phases1p3p, phasesG), nil +} + +func (wb *DaheimLaden) heartbeat(ctx context.Context, timeout time.Duration) { + for tick := time.Tick(timeout); ; { + select { + case <-tick: + case <-ctx.Done(): + return + } + + if _, err := wb.conn.ReadHoldingRegisters(dlRegSafeCurrent, 1); err != nil { + wb.log.ERROR.Println("heartbeat:", err) + } + } +} + +func (wb *DaheimLaden) setCurrent(current uint16) error { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, current) + + _, err := wb.conn.WriteMultipleRegisters(dlRegCurrentLimit, 1, b) return err } -// MaxCurrent implements the api.Charger interface -func (c *DaheimLaden) MaxCurrent(current int64) error { - defer c.reset() - - data := daheimladen.ChangeConfigurationRequest{ - Key: string(daheimladen.CHARGE_RATE), - Value: strconv.FormatInt(current, 10), - } - - uri := fmt.Sprintf("%s/cs/%s/change_config", daheimladen.BASE_URL, c.stationID) - req, err := http.NewRequest(http.MethodPost, uri, request.MarshalJSON(data)) +func (wb *DaheimLaden) getCurrent() (uint16, error) { + b, err := wb.conn.ReadHoldingRegisters(dlRegCurrentLimit, 1) if err != nil { - return err + return 0, err } - var res daheimladen.ChangeConfigurationResponse - if err = c.DoJSON(req, &res); err == nil && res.Status != string(daheimladen.CHANGE_CONFIG_ACCEPTED) { - err = fmt.Errorf("charging station refused to change max current") + return binary.BigEndian.Uint16(b), nil +} + +// Status implements the api.Charger interface +func (wb *DaheimLaden) Status() (api.ChargeStatus, error) { + b, err := wb.conn.ReadHoldingRegisters(dlRegChargingState, 1) + if err != nil { + return api.StatusNone, err + } + + s := binary.BigEndian.Uint16(b) + + switch s { + case 1: // Standby (A) + return api.StatusA, nil + case 2: // Connect (B1) + return api.StatusB, nil + case 3: // Start-up State (B2) + return api.StatusB, nil + case 4: // Charging (C) + power, err := wb.CurrentPower() + if power == 0 { + return api.StatusB, err + } + return api.StatusC, err + case 5: // Start-UP Fail (B2) + return api.StatusB, nil + case 6: // Session Terminated by EVSE + return api.StatusB, nil + default: // Other + return api.StatusNone, fmt.Errorf("invalid status: %d", s) + } +} + +// Enabled implements the api.Charger interface +func (wb *DaheimLaden) Enabled() (bool, error) { + curr, err := wb.getCurrent() + + return curr > 0, err +} + +// Enable implements the api.Charger interface +func (wb *DaheimLaden) Enable(enable bool) error { + var current uint16 + if enable { + current = wb.curr + } + + // break to avoid too fast commands + time.Sleep(time.Second) + + return wb.setCurrent(current) +} + +// MaxCurrent implements the api.Charger interface +func (wb *DaheimLaden) MaxCurrent(current int64) error { + return wb.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*DaheimLaden)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (wb *DaheimLaden) MaxCurrentMillis(current float64) error { + if current < 6 { + return fmt.Errorf("invalid current %.1f", current) + } + + curr := uint16(current * 10) + + err := wb.setCurrent(curr) + if err == nil { + wb.curr = curr } return err @@ -189,23 +238,122 @@ func (c *DaheimLaden) MaxCurrent(current int64) error { var _ api.Meter = (*DaheimLaden)(nil) // CurrentPower implements the api.Meter interface -func (c *DaheimLaden) CurrentPower() (float64, error) { - res, err := c.meterG.Get() - return float64(res.ActivePowerImport * 1e3), err +func (wb *DaheimLaden) CurrentPower() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(dlRegActivePower, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)), err } var _ api.MeterEnergy = (*DaheimLaden)(nil) -// TotalEnergy implements the api.MeterMeterEnergy interface -func (c *DaheimLaden) TotalEnergy() (float64, error) { - res, err := c.meterG.Get() - return float64(res.EnergyActiveImportRegister), err +// TotalEnergy implements the api.MeterEnergy interface +func (wb *DaheimLaden) TotalEnergy() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(dlRegTotalEnergy, 2) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint32(b)) / 10, err +} + +// getPhaseValues returns 3 sequential register values +func (wb *DaheimLaden) getPhaseValues(reg uint16) (float64, float64, float64, error) { + b, err := wb.conn.ReadHoldingRegisters(reg, 5) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + // 16-bit registers for currents/voltages spaced by 1 empty register + res[i] = float64(binary.BigEndian.Uint16(b[4*i:])) / 10 + } + + return res[0], res[1], res[2], nil } var _ api.PhaseCurrents = (*DaheimLaden)(nil) // Currents implements the api.PhaseCurrents interface -func (c *DaheimLaden) Currents() (float64, float64, float64, error) { - res, err := c.meterG.Get() - return float64(res.CurrentImportPhaseL1), float64(res.CurrentImportPhaseL2), float64(res.CurrentImportPhaseL3), err +func (wb *DaheimLaden) Currents() (float64, float64, float64, error) { + return wb.getPhaseValues(dlRegCurrents) +} + +var _ api.PhaseVoltages = (*DaheimLaden)(nil) + +// Voltages implements the api.PhaseVoltages interface +func (wb *DaheimLaden) Voltages() (float64, float64, float64, error) { + return wb.getPhaseValues(dlRegVoltages) +} + +// phases1p3p implements the api.PhaseSwitcher interface +func (wb *DaheimLaden) phases1p3p(phases int) error { + b := make([]byte, 2) + binary.BigEndian.PutUint16(b, uint16(phases)) + + _, err := wb.conn.WriteMultipleRegisters(dlRegPhaseSwitchControl, 1, b) + if err == nil { + wb.phases = uint16(phases) + } + return err +} + +// getPhases implements the api.PhaseGetter interface +func (wb *DaheimLaden) getPhases() (int, error) { + b, err := wb.conn.ReadHoldingRegisters(dlRegPhaseSwitchAction, 1) + if err != nil { + return 0, err + } + + // 0=standby, 1=changing, 2=changed + if binary.BigEndian.Uint16(b) != 1 { + b, err := wb.conn.ReadHoldingRegisters(dlRegPhaseSwitchState, 1) + if err != nil { + return 0, err + } + + wb.phases = binary.BigEndian.Uint16(b) + } + + return int(wb.phases), nil +} + +var _ api.Diagnosis = (*DaheimLaden)(nil) + +// Diagnose implements the api.Diagnosis interface +func (wb *DaheimLaden) Diagnose() { + if b, err := wb.conn.ReadHoldingRegisters(dlRegChargingState, 1); err == nil { + fmt.Printf("\tCharging Station State:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegConnectorState, 1); err == nil { + fmt.Printf("\tConnector State:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegEvseMaxCurrent, 1); err == nil { + fmt.Printf("\tEVSE Max. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegCableMaxCurrent, 1); err == nil { + fmt.Printf("\tCable Max. Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegStationId, 16); err == nil { + s, _ := utf16BEBytesAsString(b) + fmt.Printf("\tStation ID:\t%s\n", s) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegSafeCurrent, 1); err == nil { + fmt.Printf("\tSafe Current:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegCommTimeout, 1); err == nil { + fmt.Printf("\tConnection Timeout:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegCurrentLimit, 1); err == nil { + fmt.Printf("\tCurrent Limit:\t%.1fA\n", float64(binary.BigEndian.Uint16(b)/10)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegChargeControl, 1); err == nil { + fmt.Printf("\tCharge Control:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := wb.conn.ReadHoldingRegisters(dlRegChargeCmd, 1); err == nil { + fmt.Printf("\tCharge Command:\t%d\n", binary.BigEndian.Uint16(b)) + } } diff --git a/charger/daheimladen-mb_decorators.go b/charger/daheimladen_decorators.go similarity index 84% rename from charger/daheimladen-mb_decorators.go rename to charger/daheimladen_decorators.go index 616c08a14..500af40f0 100644 --- a/charger/daheimladen-mb_decorators.go +++ b/charger/daheimladen_decorators.go @@ -6,17 +6,17 @@ import ( "github.com/evcc-io/evcc/api" ) -func decorateDaheimLaden(base *DaheimLadenMB, phaseSwitcher func(int) error, phaseGetter func() (int, error)) api.Charger { +func decorateDaheimLaden(base *DaheimLaden, phaseSwitcher func(int) error, phaseGetter func() (int, error)) api.Charger { switch { case phaseSwitcher == nil: return base case phaseGetter == nil && phaseSwitcher != nil: return &struct { - *DaheimLadenMB + *DaheimLaden api.PhaseSwitcher }{ - DaheimLadenMB: base, + DaheimLaden: base, PhaseSwitcher: &decorateDaheimLadenPhaseSwitcherImpl{ phaseSwitcher: phaseSwitcher, }, @@ -24,11 +24,11 @@ func decorateDaheimLaden(base *DaheimLadenMB, phaseSwitcher func(int) error, pha case phaseGetter != nil && phaseSwitcher != nil: return &struct { - *DaheimLadenMB + *DaheimLaden api.PhaseGetter api.PhaseSwitcher }{ - DaheimLadenMB: base, + DaheimLaden: base, PhaseGetter: &decorateDaheimLadenPhaseGetterImpl{ phaseGetter: phaseGetter, }, diff --git a/templates/definition/charger/daheimladen-mb.yaml b/templates/definition/charger/daheimladen-mb.yaml deleted file mode 100644 index 8012433bc..000000000 --- a/templates/definition/charger/daheimladen-mb.yaml +++ /dev/null @@ -1,17 +0,0 @@ -template: daheimladen-mb -products: - - brand: DaheimLaden - description: - generic: Smart/Touch -requirements: - description: - de: Die Wallbox muss über eine aktuelle Firmware mit Modbus-Unterstützung verfügen. In den Einstellungen muss "Nachladen" (Smart) bzw. "RSDA" (Touch) aktiviert sein - en: Wallbox must be operated with a recent firmware including Modbus support. Furthermore, “Nachladen” (Smart) or “RSDA” (Touch) must be activated in settings. -capabilities: ["mA"] -params: - - name: host - - name: port - default: 502 -render: | - type: daheimladen-mb - uri: {{ .host }}:{{ .port }} diff --git a/templates/definition/charger/daheimladen-pro.yaml b/templates/definition/charger/daheimladen-pro.yaml index 4a61559ab..14f31b8ce 100644 --- a/templates/definition/charger/daheimladen-pro.yaml +++ b/templates/definition/charger/daheimladen-pro.yaml @@ -13,6 +13,6 @@ params: - name: port default: 502 render: | - type: daheimladen-mb + type: daheimladen uri: {{ .host }}:{{ .port }} phases1p3p: true diff --git a/templates/definition/charger/daheimladen.yaml b/templates/definition/charger/daheimladen.yaml index 2916e6fae..4a370d2d3 100644 --- a/templates/definition/charger/daheimladen.yaml +++ b/templates/definition/charger/daheimladen.yaml @@ -1,23 +1,18 @@ template: daheimladen -deprecated: true +covers: ["daheimladen-mb"] products: - brand: DaheimLaden description: - generic: Wallbox + generic: Smart/Touch requirements: description: - de: Für die Nutzung mit evcc muss die Wallbox im RFID Modus betrieben werden. - en: For use with evcc, the wallbox must be operated in RFID mode. + de: Die Wallbox muss über eine aktuelle Firmware mit Modbus-Unterstützung verfügen. In den Einstellungen muss "Nachladen" (Smart) bzw. "RSDA" (Touch) aktiviert sein + en: Wallbox must be operated with a recent firmware including Modbus support. Furthermore, “Nachladen” (Smart) or “RSDA” (Touch) must be activated in settings. +capabilities: ["mA"] params: - - name: token - help: - de: "Sie bekommen dieses Access Token vom Daheimladen Support: info@daheimladen.de." - en: "Request your access token from Daheimladen support: info@daheimladen.de." - - name: stationid - help: - de: "Die Seriennummer, welche auf dem Bildschirm der Wallbox angezeigt wird." - en: "Serial number as displayed on the wallbox display." + - name: host + - name: port + default: 502 render: | type: daheimladen - stationID: {{ .stationid }} - token: {{ .token }} + uri: {{ .host }}:{{ .port }}