From 1eeeebdadd09fe6fc152c86451ab480ea60d7a2c Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 9 Aug 2024 09:24:01 +0200 Subject: [PATCH 01/35] Modbus: close connection on error (#15319) --- util/modbus/connection.go | 80 ++++++++++++++++++++------------------- util/modbus/log.go | 3 +- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/util/modbus/connection.go b/util/modbus/connection.go index 672a44ffd2..7dad55b20c 100644 --- a/util/modbus/connection.go +++ b/util/modbus/connection.go @@ -1,18 +1,15 @@ package modbus import ( - "sync" "time" - "github.com/grid-x/modbus" "github.com/volkszaehler/mbmd/meters" ) // Connection is a logical modbus connection per slave ID sharing a physical connection type Connection struct { + *logger meters.Connection - mu sync.Mutex - logger *logger logical meters.Logger delay time.Duration } @@ -42,72 +39,79 @@ func (c *Connection) Timeout(timeout time.Duration) { } } -func (c *Connection) Logger(l modbus.Logger) { - c.mu.Lock() - defer c.mu.Unlock() - - c.logical = l -} - -func (c *Connection) prepare() { - c.mu.Lock() - defer c.mu.Unlock() - +func (c *Connection) exec(fun func() ([]byte, error)) ([]byte, error) { time.Sleep(c.delay) - c.logger.Logger(c.logical) + return c.WithLogger(c.logical, func() ([]byte, error) { + b, err := fun() + if err != nil { + c.Connection.Close() + } + return b, err + }) } func (c *Connection) ReadCoils(address, quantity uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().ReadCoils(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadCoils(address, quantity) + }) } func (c *Connection) WriteSingleCoil(address, value uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().WriteSingleCoil(address, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteSingleCoil(address, value) + }) } func (c *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().ReadInputRegisters(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadInputRegisters(address, quantity) + }) } func (c *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().ReadHoldingRegisters(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadHoldingRegisters(address, quantity) + }) } func (c *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) { - c.prepare() - return c.ModbusClient().WriteSingleRegister(address, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteSingleRegister(address, value) + }) } func (c *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) { - c.prepare() - return c.ModbusClient().WriteMultipleRegisters(address, quantity, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteMultipleRegisters(address, quantity, value) + }) } func (c *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) { - c.prepare() - return c.ModbusClient().ReadDiscreteInputs(address, quantity) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadDiscreteInputs(address, quantity) + }) } func (c *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) { - c.prepare() - return c.ModbusClient().WriteMultipleCoils(address, quantity, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().WriteMultipleCoils(address, quantity, value) + }) } func (c *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) { - c.prepare() - return c.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value) + }) } func (c *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) { - c.prepare() - return c.ModbusClient().MaskWriteRegister(address, andMask, orMask) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().MaskWriteRegister(address, andMask, orMask) + }) } func (c *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) { - c.prepare() - return c.ModbusClient().ReadFIFOQueue(address) + return c.exec(func() ([]byte, error) { + return c.ModbusClient().ReadFIFOQueue(address) + }) } diff --git a/util/modbus/log.go b/util/modbus/log.go index 59495eb20b..be19a94e84 100644 --- a/util/modbus/log.go +++ b/util/modbus/log.go @@ -12,11 +12,12 @@ type logger struct { logger meters.Logger } -func (l *logger) Logger(logger modbus.Logger) { +func (l *logger) WithLogger(logger modbus.Logger, fun func() ([]byte, error)) ([]byte, error) { l.mu.Lock() defer l.mu.Unlock() l.logger = logger + return fun() } func (l *logger) Printf(format string, v ...interface{}) { From fbeb56a62098140f19a5a1e19e467317e1a1d319 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 9 Aug 2024 10:17:31 +0200 Subject: [PATCH 02/35] chore: fix deadlock in logger --- util/modbus/log.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/util/modbus/log.go b/util/modbus/log.go index be19a94e84..589c2615a2 100644 --- a/util/modbus/log.go +++ b/util/modbus/log.go @@ -20,10 +20,9 @@ func (l *logger) WithLogger(logger modbus.Logger, fun func() ([]byte, error)) ([ return fun() } +// Printf implements modbus.Logger interface. +// Must always be called while being wrapped in WithLogger, hence the lock is held. func (l *logger) Printf(format string, v ...interface{}) { - l.mu.RLock() - defer l.mu.RUnlock() - if l.logger != nil { l.logger.Printf(format, v...) } From 80b2c79e0dacaed90b723f0e7aa4db4c16311347 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 9 Aug 2024 16:04:54 +0200 Subject: [PATCH 03/35] chore: fix race condition --- charger/eebus.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/charger/eebus.go b/charger/eebus.go index 8e8ae5ace8..74ed78c7f6 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -111,13 +111,6 @@ func NewEEBus(ski string, hasMeter, hasChargedEnergy, vasVW bool) (api.Charger, return c, nil } -func (c *EEBus) setEvEntity(entity spineapi.EntityRemoteInterface) { - c.mux.Lock() - defer c.mux.Unlock() - - c.ev = entity -} - func (c *EEBus) evEntity() spineapi.EntityRemoteInterface { c.mux.RLock() defer c.mux.RUnlock() @@ -139,15 +132,18 @@ var _ eebus.Device = (*EEBus)(nil) // UseCaseEvent implements the eebus.Device interface func (c *EEBus) UseCaseEvent(device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event eebusapi.EventType) { - switch event { + c.mux.Lock() + defer c.mux.Unlock() + // EV + switch event { case evcc.EvConnected: - c.setEvEntity(entity) + c.ev = entity c.reconnect = true c.currentLimit = -1 case evcc.EvDisconnected: - c.setEvEntity(nil) + c.ev = nil c.currentLimit = -1 } } From ee2284a6aee15fef0ea35d1c3f330bca4a1433ae Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 9 Aug 2024 16:07:00 +0200 Subject: [PATCH 04/35] chore: fix race condition --- charger/eebus.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/charger/eebus.go b/charger/eebus.go index 74ed78c7f6..76fef4e854 100644 --- a/charger/eebus.go +++ b/charger/eebus.go @@ -557,6 +557,9 @@ var _ api.CurrentGetter = (*EEBus)(nil) // GetMaxCurrent implements the api.CurrentGetter interface func (c *EEBus) GetMaxCurrent() (float64, error) { + c.mux.RLock() + defer c.mux.RUnlock() + if c.currentLimit == -1 { return 0, api.ErrNotAvailable } From e631aadc3b552539abff21f1a57142d988c1174c Mon Sep 17 00:00:00 2001 From: Herbert23 <48907460+Herbert23@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:10:54 +0200 Subject: [PATCH 05/35] Add ext meters for logging (#15049) Co-authored-by: Marcel Bialojahn --- core/keys/site.go | 2 ++ core/site.go | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/core/keys/site.go b/core/keys/site.go index 07254c0db8..3d5417a86c 100644 --- a/core/keys/site.go +++ b/core/keys/site.go @@ -29,11 +29,13 @@ const ( TariffPriceLoadpoints = "tariffPriceLoadpoints" Vehicles = "vehicles" Circuits = "circuits" + Ext = "ext" // meters GridMeter = "gridMeter" PvMeters = "pvMeters" BatteryMeters = "batteryMeters" + ExtMeters = "extMeters" AuxMeters = "auxMeters" // battery settings diff --git a/core/site.go b/core/site.go index 6a5a135059..f663c4e62d 100644 --- a/core/site.go +++ b/core/site.go @@ -82,6 +82,7 @@ type Site struct { gridMeter api.Meter // Grid usage meter pvMeters []api.Meter // PV generation meters batteryMeters []api.Meter // Battery charging meters + extMeters []api.Meter // External meters - for monitoring only auxMeters []api.Meter // Auxiliary meters // battery settings @@ -112,6 +113,7 @@ type MetersConfig struct { GridMeterRef string `mapstructure:"grid"` // Grid usage meter PVMetersRef []string `mapstructure:"pv"` // PV meter BatteryMetersRef []string `mapstructure:"battery"` // Battery charging meter + ExtMetersRef []string `mapstructure:"ext"` // Meters used only for monitoring AuxMetersRef []string `mapstructure:"aux"` // Auxiliary meters } @@ -209,6 +211,15 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif site.log.WARN.Println("battery configured but residualPower is missing or <= 0 (add residualPower: 100 to site), see https://docs.evcc.io/en/docs/reference/configuration/site#residualpower") } + // Meters used only for monitoring + for _, ref := range site.Meters.ExtMetersRef { + dev, err := config.Meters().ByName(ref) + if err != nil { + return err + } + site.extMeters = append(site.extMeters, dev.Instance()) + } + // auxiliary meters for _, ref := range site.Meters.AuxMetersRef { dev, err := config.Meters().ByName(ref) @@ -258,6 +269,9 @@ func (site *Site) restoreMetersAndTitle() { if v, err := settings.String(keys.BatteryMeters); err == nil && v != "" { site.Meters.BatteryMetersRef = append(site.Meters.BatteryMetersRef, filterConfigurable(strings.Split(v, ","))...) } + if v, err := settings.String(keys.ExtMeters); err == nil && v != "" { + site.Meters.ExtMetersRef = append(site.Meters.ExtMetersRef, filterConfigurable(strings.Split(v, ","))...) + } if v, err := settings.String(keys.AuxMeters); err == nil && v != "" { site.Meters.AuxMetersRef = append(site.Meters.AuxMetersRef, filterConfigurable(strings.Split(v, ","))...) } @@ -469,6 +483,39 @@ func (site *Site) updatePvMeters() { site.publish(keys.Pv, mm) } +// updateExtMeters updates ext meters. All measurements are optional. +func (site *Site) updateExtMeters() { + if len(site.extMeters) == 0 { + return + } + + mm := make([]meterMeasurement, len(site.extMeters)) + + for i, meter := range site.extMeters { + // ext power + power, err := backoff.RetryWithData(meter.CurrentPower, bo()) + if err != nil { + site.log.ERROR.Printf("ext meter %d power: %v", i+1, err) + } + + // ext energy + var energy float64 + if m, ok := meter.(api.MeterEnergy); err == nil && ok { + energy, err = m.TotalEnergy() + if err != nil { + site.log.ERROR.Printf("ext meter %d energy: %v", i+1, err) + } + } + + mm[i] = meterMeasurement{ + Power: power, + Energy: energy, + } + } + + // Publishing will be done in separate PR +} + // updateBatteryMeters updates battery meters. Power is retried, other measurements are optional. func (site *Site) updateBatteryMeters() error { if len(site.batteryMeters) == 0 { @@ -615,6 +662,7 @@ func (site *Site) updateMeters() error { if err := site.updateBatteryMeters(); err != nil { return err } + site.updateExtMeters() return site.updateGridMeter() } From e0ccda0db70e9d9c7c0efe6040942c0afcc4d479 Mon Sep 17 00:00:00 2001 From: Jorge <4804546+jomach@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:18:19 +0200 Subject: [PATCH 06/35] Add EVBox Livo (#15193) --- templates/definition/charger/evbox-livo.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 templates/definition/charger/evbox-livo.yaml diff --git a/templates/definition/charger/evbox-livo.yaml b/templates/definition/charger/evbox-livo.yaml new file mode 100644 index 0000000000..ae4f43fade --- /dev/null +++ b/templates/definition/charger/evbox-livo.yaml @@ -0,0 +1,14 @@ +template: livo +products: + - brand: EVBox + description: + generic: Livo +requirements: + evcc: ["eebus"] + description: + de: Das Gerät benötigt eine feste IP Adresse. + en: The device requires a fixed IP addres. +params: + - preset: eebus +render: | + {{ include "eebus" . }} From 12d61b53fdd0881d2dcf41e4157c8801cf6bb200 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 10 Aug 2024 11:01:15 +0200 Subject: [PATCH 07/35] Tesla: log proxy --- vehicle/tesla.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vehicle/tesla.go b/vehicle/tesla.go index fd5525968f..d51c6183e7 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -111,6 +111,8 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { } tcc.SetBaseUrl(cc.CommandProxy) + log.INFO.Println("!! tesla proxy for %s:", vehicle.DisplayName, cc.CommandProxy) + v := &Tesla{ embed: &cc.embed, Provider: tesla.NewProvider(vehicle, cc.Cache), From 3d401bd18f58d49aeda76073956db70fe125c266 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 10 Aug 2024 11:05:04 +0200 Subject: [PATCH 08/35] chore: fix print --- vehicle/tesla.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/tesla.go b/vehicle/tesla.go index d51c6183e7..e34d96c544 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -111,7 +111,7 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { } tcc.SetBaseUrl(cc.CommandProxy) - log.INFO.Println("!! tesla proxy for %s:", vehicle.DisplayName, cc.CommandProxy) + log.INFO.Printf("!! tesla proxy for %s:", vehicle.DisplayName, cc.CommandProxy) v := &Tesla{ embed: &cc.embed, From baf15a62e8de4af0cde630bdc8db030039693a71 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 10 Aug 2024 11:07:39 +0200 Subject: [PATCH 09/35] chore: log modbus timeouts --- util/modbus/connection.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/util/modbus/connection.go b/util/modbus/connection.go index 7dad55b20c..122ed0c501 100644 --- a/util/modbus/connection.go +++ b/util/modbus/connection.go @@ -1,6 +1,7 @@ package modbus import ( + "fmt" "time" "github.com/volkszaehler/mbmd/meters" @@ -40,11 +41,14 @@ func (c *Connection) Timeout(timeout time.Duration) { } func (c *Connection) exec(fun func() ([]byte, error)) ([]byte, error) { - time.Sleep(c.delay) return c.WithLogger(c.logical, func() ([]byte, error) { + time.Sleep(c.delay) + start := time.Now() + b, err := fun() if err != nil { c.Connection.Close() + fmt.Println("!!", time.Since(start), err) } return b, err }) From db560b63c3236473761a2cdd0af9e6f9d2bb5492 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 10 Aug 2024 11:09:17 +0200 Subject: [PATCH 10/35] chore: fix log --- vehicle/tesla.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vehicle/tesla.go b/vehicle/tesla.go index e34d96c544..6a0c8c74c1 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -111,7 +111,7 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { } tcc.SetBaseUrl(cc.CommandProxy) - log.INFO.Printf("!! tesla proxy for %s:", vehicle.DisplayName, cc.CommandProxy) + log.INFO.Printf("!! tesla proxy for %s: %s", vehicle.DisplayName, cc.CommandProxy) v := &Tesla{ embed: &cc.embed, From a2e36a2efb13a3ecadc38c66f227b1d11afa16b3 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 10 Aug 2024 18:19:48 +0200 Subject: [PATCH 11/35] docs: ensure help texts are not multiline (#15335) --- util/templates/defaults.yaml | 50 +++++------------------------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml index 32c24a7d8a..c3dc3885d0 100644 --- a/util/templates/defaults.yaml +++ b/util/templates/defaults.yaml @@ -128,30 +128,6 @@ params: advanced: true type: duration example: 5m - - name: cloud - description: - de: evcc Cloud - en: evcc Cloud - help: - de: | - Anstatt auf die Online API des Fahrzeugherstellers von der lokalen Installation zuzugreifen, - wird dies über die evcc Cloud gemacht. Die Zugangsdaten und VIN werden sicher und verschlüsselt - übertragen und in der Cloud nicht gespeichert. - - Die Vorteile sind: - - Falls sich etwas an der Online API des Fahrzeugherstellers ändert, können Updates für alle Benutzer viel schneller zur Verfügung gestellt werden - - Man muss nicht auf eine neue Version von evcc warten und dann ein lokales Update von evcc durchführen - en: | - Instead of connecting to the vehicle manufacturers online API from your local installation, - this will use the evcc Cloud instead. Your credentials and VIN will be transfered encrypted and - securely and won't be stored in the cloud. - - The benefits are: - - If the vehicles online API changes, we can update this for all users more quickly - - No waiting for a new evcc version in this case and then requiring an update - requirements: - evcc: ["sponsorship"] - type: bool - name: timeout description: de: Zeitüberschreitung @@ -376,12 +352,8 @@ presets: - name: stationid type: string help: - en: | - Station ID of the charging point. Only required if multiple OCPP charging stations are set up to assign them correctly. A single OCPP charging station can also be automatically assigned. - Note: In exceptional cases, it may be necessary to manually append this ID to the OCPP URL of the charging station in the form ws://:8887/. Most charging stations automatically add the ID internally. - de: | - Station ID des Ladepunktes. Nur erforderlich wenn mehrere OCPP-Ladestationen eingerichtet sind um diese korrekt zuzuweisen. Eine einzelne OCPP-Ladestation kann auch automatisch zugeordnet werden. - Hinweis: In Ausnahmefällen kann es erforderlich sein, diese ID manuell an die OCPP-URL der Ladestation in der Form ws://:8887/ anzuhängen. Die meisten Ladestationen fügen die ID intern automatisch hinzu. + en: "Station ID of the charging point. Only required if multiple OCPP charging stations are set up to assign them correctly. A single OCPP charging station can also be automatically assigned. Note: In exceptional cases, it may be necessary to manually append this ID to the OCPP URL of the charging station in the form ws://:8887/. Most charging stations automatically add the ID internally." + de: "Station ID des Ladepunktes. Nur erforderlich wenn mehrere OCPP-Ladestationen eingerichtet sind um diese korrekt zuzuweisen. Eine einzelne OCPP-Ladestation kann auch automatisch zugeordnet werden. Hinweis: In Ausnahmefällen kann es erforderlich sein, diese ID manuell an die OCPP-URL der Ladestation in der Form ws://:8887/ anzuhängen. Die meisten Ladestationen fügen die ID intern automatisch hinzu." example: EVB-P12354 - name: connector help: @@ -395,16 +367,8 @@ presets: de: Immer eine Remote-Transaktion starten (RemoteStartTransaction) sobald ein Fahrzeug angeschlossen wird en: Always start a remote transaction (RemoteStartTransaction) as soon as a vehicle is connected help: - de: | - Diese Option nur aktivieren wenn keinerlei Möglichkeit besteht Transaktionen seitens des Ladepunktes zu initiieren! - Das ist nur der Fall wenn z. B. kein RFID-Lesegerät vorhanden ist und Ladevorgänge grundsätzlich einzeln per App freigeschaltet werden müssten. - Normalerweise sollte der Ladepunkt am Gerät immer so konfiguriert werden, dass entweder eine RFID-Karte zur Freischaltung verwendet wird oder der Ladepunkt auf "Autostart", "Freies Laden" o.ä. eingestellt ist. - Zunächst die Dokumentation und die Konfigurationsmöglichkeiten des Ladepunktes prüfen, ggf. beim Hersteller nachfragen! - en: | - Only enable this option if there is no way to initiate transactions from the charger side! - This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. - Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. - First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! + de: Diese Option nur aktivieren wenn keinerlei Möglichkeit besteht Transaktionen seitens des Ladepunktes zu initiieren! Das ist nur der Fall wenn z. B. kein RFID-Lesegerät vorhanden ist und Ladevorgänge grundsätzlich einzeln per App freigeschaltet werden müssten. Normalerweise sollte der Ladepunkt am Gerät immer so konfiguriert werden, dass entweder eine RFID-Karte zur Freischaltung verwendet wird oder der Ladepunkt auf "Autostart", "Freies Laden" o.ä. eingestellt ist. Zunächst die Dokumentation und die Konfigurationsmöglichkeiten des Ladepunktes prüfen, ggf. beim Hersteller nachfragen! + en: Only enable this option if there is no way to initiate transactions from the charger side! This is only the case if e.g. no RFID reader is available and charging processes would have to be released individually via app. Normally, the charger should always be configured at the device so that either an RFID card is used for activation or the charger is set to "Autostart", "Free Charging" or similar. First check the documentation and configuration possibilities of the charger, ask the manufacturer if necessary! - name: idtag advanced: true type: string @@ -412,10 +376,8 @@ presets: en: Identifier used for authentication of external transactions (RemoteStartTransaction) de: Identifikator zur Authentifizierung von externen Transaktionen (RemoteStartTransaction) help: - de: | - Diese Option ist nur in Ausnahmefällen erforderlich wenn der Ladepunkt für die Annahme externer Transaktionen einen spezifischen Token erfordert. - en: | - This option is only required in exceptional cases if the charger requires a specific token for accepting external transactions. + de: Diese Option ist nur in Ausnahmefällen erforderlich wenn der Ladepunkt für die Annahme externer Transaktionen einen spezifischen Token erfordert. + en: This option is only required in exceptional cases if the charger requires a specific token for accepting external transactions. example: evcc - name: connecttimeout advanced: true From 260c13e6d402e51fa4f8fa9dba071af194ef0dbe Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 11 Aug 2024 12:06:32 +0200 Subject: [PATCH 12/35] Victron: clarify docs --- templates/definition/charger/victron-evcs.yaml | 2 +- templates/definition/charger/victron.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/definition/charger/victron-evcs.yaml b/templates/definition/charger/victron-evcs.yaml index 56c6f69d4e..690ecee885 100644 --- a/templates/definition/charger/victron-evcs.yaml +++ b/templates/definition/charger/victron-evcs.yaml @@ -2,7 +2,7 @@ template: victron-evcs products: - brand: Victron description: - generic: EV charging station + generic: EV Charging Station requirements: evcc: ["sponsorship"] description: diff --git a/templates/definition/charger/victron.yaml b/templates/definition/charger/victron.yaml index 7dd26adc51..fdf3911491 100644 --- a/templates/definition/charger/victron.yaml +++ b/templates/definition/charger/victron.yaml @@ -2,7 +2,7 @@ template: victron products: - brand: Victron description: - generic: EV Charging Station + generic: EV Charging Station (via GX) requirements: evcc: ["sponsorship"] description: From 061beab0827d006f19060412d94effcd1847859f Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 11 Aug 2024 13:04:46 +0200 Subject: [PATCH 13/35] cli/charger: support mA currents (#15341) --- cmd/charger.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/charger.go b/cmd/charger.go index 06f3cba49a..ae6c2f9cbd 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -19,7 +19,7 @@ var chargerCmd = &cobra.Command{ func init() { rootCmd.AddCommand(chargerCmd) - chargerCmd.Flags().IntP(flagCurrent, "i", 0, flagCurrentDescription) + chargerCmd.Flags().Float64P(flagCurrent, "i", 0, flagCurrentDescription) //lint:ignore SA1019 as Title is safe on ascii chargerCmd.Flags().BoolP(flagEnable, "e", false, strings.Title(flagEnable)) //lint:ignore SA1019 as Title is safe on ascii @@ -60,13 +60,19 @@ func runCharger(cmd *cobra.Command, args []string) { if flag := cmd.Flags().Lookup(flagCurrent); flag.Changed { flagUsed = true - current, err := strconv.ParseInt(flag.Value.String(), 10, 64) + current, err := strconv.ParseFloat(flag.Value.String(), 64) if err != nil { log.ERROR.Fatalln(err) } - if err := v.MaxCurrent(current); err != nil { - log.ERROR.Println("set current:", err) + if vv, ok := v.(api.ChargerEx); ok { + if err := vv.MaxCurrentMillis(current); err != nil { + log.ERROR.Println("set current:", err) + } + } else { + if err := v.MaxCurrent(int64(current)); err != nil { + log.ERROR.Println("set current:", err) + } } } From b336c4d70dc902cf7c9bb3e1cb5314b7ef9b5ad7 Mon Sep 17 00:00:00 2001 From: wobu Date: Sun, 11 Aug 2024 13:49:12 +0200 Subject: [PATCH 14/35] Add NrgKick Gen2 (#15138) --- README.md | 2 +- charger/nrggen2.go | 336 ++++++++++++++++++++++ charger/nrggen2_decorators.go | 35 +++ templates/definition/charger/nrggen2.yaml | 26 ++ 4 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 charger/nrggen2.go create mode 100644 charger/nrggen2_decorators.go create mode 100644 templates/definition/charger/nrggen2.yaml diff --git a/README.md b/README.md index 3150a206b8..75ddae3530 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ evcc is an extensible EV Charge Controller and home energy management system. Fe - simple and clean user interface - wide range of supported [chargers](https://docs.evcc.io/docs/devices/chargers): - - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more + - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Mennekes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), NRGKick Gen2,[openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more - experimental EEBus support (Elli, PMCC) - experimental OCPP support - Build-your-own: Phoenix Contact (includes ESL Walli), [EVSE DIN](http://evracing.cz/simple-evse-wallbox) diff --git a/charger/nrggen2.go b/charger/nrggen2.go new file mode 100644 index 0000000000..1c3c0ccde4 --- /dev/null +++ b/charger/nrggen2.go @@ -0,0 +1,336 @@ +package charger + +import ( + "encoding/binary" + "fmt" + "math" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/modbus" + "github.com/evcc-io/evcc/util/sponsor" + "github.com/spf13/cast" + "github.com/volkszaehler/mbmd/encoding" +) + +// https://www.nrgkick.com/wp-content/uploads/2024/07/local_api_docu_simulate.html + +// NRGKickGen2 charger implementation +type NRGKickGen2 struct { + conn *modbus.Connection +} + +const ( + // All register use LittleEndian + // Read only (0x03) + nrgKickGen2Serial = 0 // 11 regs + nrgKickGen2ModelType = 11 // 16 regs + nrgKickGen2MaxPhases = 36 + nrgKickGen2SoftwareVersionSM = 122 // 8 regs + // Read (0x03) / Write (0x06, 0x16) Registers + nrgKickGen2ChargingCurrent = 194 // A, factor 10 + nrgKickGen2Enabled = 195 + nrgKickGen2Phases = 198 + // Read only (0x03) + nrgKickGen2TotalChargedEnergy = 199 // Wh, 4 regs + nrgKickGen2ChargedEnergy = 203 // Wh, 2 regs + nrgKickGen2TotalActivePower = 210 // W, 2 regs, factor 1000 + nrgKickGen2PhaseVoltages = 217 // factor 100 + nrgKickGen2PhaseCurrents = 220 // factor 1000 + nrgKickGen2RegStatus = 251 + nrgKickGen2RegRelais = 253 + nrgKickGen2RegRCD = 255 + nrgKickGen2RegWarning = 256 + nrgKickGen2RegError = 257 +) + +func init() { + registry.Add("nrggen2", NewNRGKickGen2FromConfig) +} + +//go:generate go run ../cmd/tools/decorate.go -f decorateNRGKickGen2 -b *NRGKickGen2 -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" + +// NewNRGKickGen2FromConfig creates a NRGKickGen2 charger from generic config +func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error) { + cc := struct { + modbus.TcpSettings `mapstructure:",squash"` + Phases1p3p bool + }{ + TcpSettings: modbus.TcpSettings{ + ID: 1, // default + }, + Phases1p3p: false, + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + nrg, err := NewNRGKickGen2(cc.URI, cc.ID) + if err != nil { + return nil, err + } + + var ( + phasesS func(int) error + ) + + if cc.Phases1p3p { + // user could have an adapter plug which doesn't support 3 phases + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2MaxPhases, 1); err == nil { + if maxPhases := encoding.Uint16(b); maxPhases > 1 { + phasesS = nrg.phases1p3p + } + } + } + + return decorateNRGKickGen2(nrg, phasesS), nil +} + +// NewNRGKickGen2 creates NRGKickGen2 charger +func NewNRGKickGen2(uri string, slaveID uint8) (*NRGKickGen2, error) { + conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID) + if err != nil { + return nil, err + } + + if !sponsor.IsAuthorized() { + return nil, api.ErrSponsorRequired + } + + log := util.NewLogger("nrggen2") + conn.Logger(log.TRACE) + + nrg := &NRGKickGen2{ + conn: conn, + } + + return nrg, nil +} + +// Status implements the api.Charger interface +func (nrg *NRGKickGen2) Status() (api.ChargeStatus, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegStatus, 1) + if err != nil { + return api.StatusNone, err + } + + // 0 - "UNKNOWN", + // 1 - "STANDBY", + // 2 - "CONNECTED", + // 3 - "CHARGING", + // 6 - "ERROR", + // 7 - "WAKEUP" + switch status := binary.BigEndian.Uint16(b); status { + case 0: + return api.StatusNone, nil + case 1: + return api.StatusA, nil + case 2: + return api.StatusB, nil + case 3: + return api.StatusC, nil + case 6: + // 0 - "NO_ERROR", + // 1 - "GENERAL_ERROR", + // 2 - "32A_ATTACHMENT_ON_16A_UNIT", + // 3 - "VOLTAGE_DROP_DETECTED", + // 4 - "UNPLUG_DETECTION_TRIGGERED", + // 5 - "TYPE2_NOT_AUTHORIZED", + // 16 - "RESIDUAL_CURRENT_DETECTED", + // 32 - "CP_SIGNAL_VOLTAGE_ERROR", + // 33 - "CP_SIGNAL_IMPERMISSIBLE", + // 34 - "EV_DIODE_FAULT", + // 48 - "PE_SELF_TEST_FAILED", + // 49 - "RCD_SELF_TEST_FAILED", + // 50 - "RELAY_SELF_TEST_FAILED", + // 51 - "PE_AND_RCD_SELF_TEST_FAILED", + // 52 - "PE_AND_RELAY_SELF_TEST_FAILED", + // 53 - "RCD_AND_RELAY_SELF_TEST_FAILED", + // 54 - "PE_AND_RCD_AND_RELAY_SELF_TEST_FAILED", + // 64 - "SUPPLY_VOLTAGE_ERROR", + // 65 - "PHASE_SHIFT_ERROR", + // 66 - "OVERVOLTAGE_DETECTED", + // 67 - "UNDERVOLTAGE_DETECTED", + // 68 - "OVERVOLTAGE_WITHOUT_PE_DETECTED", + // 69 - "UNDERVOLTAGE_WITHOUT_PE_DETECTED", + // 70 - "UNDERFREQUENCY_DETECTED", + // 71 - "OVERFREQUENCY_DETECTED", + // 72 - "UNKNOWN_FREQUENCY_TYPE", + // 73 - "UNKNOWN_GRID_TYPE", + // 80 - "GENERAL_OVERTEMPERATURE", + // 81 - "HOUSING_OVERTEMPERATURE", + // 82 - "ATTACHMENT_OVERTEMPERATURE", + // 83 - "DOMESTIC_PLUG_OVERTEMPERATURE", + // x - "UNKNOWN" + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegError, 1) + if err != nil { + return api.StatusNone, err + } + return api.StatusF, fmt.Errorf("%d", error) + case 7: + return api.StatusB, nil + default: + return api.StatusNone, fmt.Errorf("unhandled status type") + } +} + +// Enabled implements the api.Charger interface +func (nrg *NRGKickGen2) Enabled() (bool, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Enabled, 1) + if err != nil { + return false, err + } + + // 0 = no charge pause, 1 = charge pause + return binary.BigEndian.Uint16(b) == 0, nil +} + +// Enable implements the api.Charger interface +func (nrg *NRGKickGen2) Enable(enable bool) error { + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2Enabled, cast.ToUint16(!enable)) + return err +} + +// MaxCurrent implements the api.Charger interface +func (nrg *NRGKickGen2) MaxCurrent(current int64) error { + return nrg.MaxCurrentMillis(float64(current)) +} + +var _ api.ChargerEx = (*NRGKickGen2)(nil) + +// MaxCurrentMillis implements the api.ChargerEx interface +func (nrg *NRGKickGen2) MaxCurrentMillis(current float64) error { + if current < 6 { + return fmt.Errorf("invalid current %.1f", current) + } + + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2ChargingCurrent, uint16(math.Trunc(current*10))) + return err +} + +func (nrg *NRGKickGen2) GetMaxCurrent() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ChargingCurrent, 1) + if err != nil { + return 0, err + } + + return float64(binary.BigEndian.Uint16(b)) / 10, nil +} + +var _ api.Meter = (*NRGKickGen2)(nil) + +// CurrentPower implements the api.Meter interface +func (nrg *NRGKickGen2) CurrentPower() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2TotalActivePower, 2) + if err != nil { + return 0, err + } + + return float64(encoding.Int32LswFirst(b)) * 1e-3, nil +} + +var _ api.MeterEnergy = (*NRGKickGen2)(nil) + +// TotalEnergy implements the api.MeterEnergy interface +func (nrg *NRGKickGen2) TotalEnergy() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2TotalChargedEnergy, 4) + if err != nil { + return 0, err + } + + return float64(encoding.Uint64LswFirst(b)) * 1e-3, nil +} + +var _ api.PhaseCurrents = (*NRGKickGen2)(nil) + +// Currents implements the api.PhaseCurrents interface +func (nrg *NRGKickGen2) Currents() (float64, float64, float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2PhaseCurrents, 3) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) * 1e-3 + } + + return res[0], res[1], res[2], nil +} + +var _ api.PhaseVoltages = (*NRGKickGen2)(nil) + +// Currents implements the api.PhaseVoltages interface +func (nrg *NRGKickGen2) Voltages() (float64, float64, float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2PhaseVoltages, 3) + if err != nil { + return 0, 0, 0, err + } + + var res [3]float64 + for i := range res { + res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) * 1e-2 + } + + return res[0], res[1], res[2], nil +} + +var _ api.ChargeRater = (*NRGKickGen2)(nil) + +func (nrg *NRGKickGen2) ChargedEnergy() (float64, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ChargedEnergy, 2) + if err != nil { + return 0, err + } + + return float64(encoding.Uint32LswFirst(b)) * 1e-3, nil +} + +// Phases1p3p implements the api.PhaseSwitcher interface +func (nrg *NRGKickGen2) phases1p3p(phases int) error { + // this can return an error, if phase switching isn't activated via the App + _, err := nrg.conn.WriteSingleRegister(nrgKickGen2Phases, uint16(phases)) + return err +} + +var _ api.PhaseGetter = (*NRGKickGen2)(nil) + +func (nrg *NRGKickGen2) GetPhases() (int, error) { + b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Phases, 1) + if err != nil { + return 0, err + } + + return int(binary.BigEndian.Uint16(b)), nil +} + +var _ api.Diagnosis = (*NRGKickGen2)(nil) + +// Diagnose implements the api.Diagnosis interface +func (nrg *NRGKickGen2) Diagnose() { + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2Serial, 11); err == nil { + fmt.Printf("\tSerial:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2ModelType, 16); err == nil { + fmt.Printf("\tModel:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2SoftwareVersionSM, 8); err == nil { + fmt.Printf("\tSmartModule Version:\t%s\n", bytesAsString(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegStatus, 1); err == nil { + fmt.Printf("\tStatus:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegRelais, 1); err == nil { + fmt.Printf("\tRelais Switching:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegRCD, 1); err == nil { + fmt.Printf("\tRCD:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegWarning, 1); err == nil { + fmt.Printf("\tWarning:\t%d\n", binary.BigEndian.Uint16(b)) + } + if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2RegError, 1); err == nil { + fmt.Printf("\tError:\t%d\n", binary.BigEndian.Uint16(b)) + } +} diff --git a/charger/nrggen2_decorators.go b/charger/nrggen2_decorators.go new file mode 100644 index 0000000000..c2db71ca97 --- /dev/null +++ b/charger/nrggen2_decorators.go @@ -0,0 +1,35 @@ +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 decorateNRGKickGen2(base *NRGKickGen2, phaseSwitcher func(int) error) api.Charger { + switch { + case phaseSwitcher == nil: + return base + + case phaseSwitcher != nil: + return &struct { + *NRGKickGen2 + api.PhaseSwitcher + }{ + NRGKickGen2: base, + PhaseSwitcher: &decorateNRGKickGen2PhaseSwitcherImpl{ + phaseSwitcher: phaseSwitcher, + }, + } + } + + return nil +} + +type decorateNRGKickGen2PhaseSwitcherImpl struct { + phaseSwitcher func(int) error +} + +func (impl *decorateNRGKickGen2PhaseSwitcherImpl) Phases1p3p(p0 int) error { + return impl.phaseSwitcher(p0) +} diff --git a/templates/definition/charger/nrggen2.yaml b/templates/definition/charger/nrggen2.yaml new file mode 100644 index 0000000000..aa5ac0ac68 --- /dev/null +++ b/templates/definition/charger/nrggen2.yaml @@ -0,0 +1,26 @@ +template: nrggen2 +products: + - brand: NRGKick + description: + generic: Gen2 +requirements: + evcc: ["sponsorship"] +capabilities: ["1p3p", "mA"] +params: + - name: modbus + choice: ["tcpip"] + id: 1 + - name: phases1p3p + type: bool + default: false + advanced: true + help: + de: Aktiviert Phasenumschaltung. Erweiterte Funktion "Phasenumschaltung" muss in der NRGkick App aktiviert sein. + en: Activates phase switching. Extended feature "Phase Switching" must be activated in the NRGKick app. +render: | + type: nrggen2 + {{- include "modbus" . }} + {{- if ne .phases1p3p "false" }} + phases1p3p: true + {{- end }} + From 38826a0035d5de85c98ea6b4e73a4657a2d287c4 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 11 Aug 2024 14:08:57 +0200 Subject: [PATCH 15/35] chore: switch to coder/websocket (#15340) --- charger/pulsatrix.go | 2 +- go.mod | 3 ++- go.sum | 6 ++++-- provider/socket.go | 2 +- provider/socket_test.go | 2 +- server/socket.go | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/charger/pulsatrix.go b/charger/pulsatrix.go index 5ff0ee4422..4dae62ec02 100644 --- a/charger/pulsatrix.go +++ b/charger/pulsatrix.go @@ -35,11 +35,11 @@ import ( "time" "github.com/cenkalti/backoff/v4" + "github.com/coder/websocket" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/sponsor" - "nhooyr.io/websocket" ) // pulsatrix charger implementation diff --git a/go.mod b/go.mod index cb29bf5ee9..9bf4cf9047 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/bogosj/tesla v1.3.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 + github.com/coder/websocket v1.8.12 github.com/containrrr/shoutrrr v0.8.0 github.com/coreos/go-oidc/v3 v3.10.0 github.com/denisbrodbeck/machineid v1.0.1 @@ -105,7 +106,6 @@ require ( google.golang.org/protobuf v1.34.2 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.25.10 - nhooyr.io/websocket v1.8.11 ) require ( @@ -193,6 +193,7 @@ require ( modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.30.1 // indirect + nhooyr.io/websocket v1.8.17 // indirect ) replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467 diff --git a/go.sum b/go.sum index 7c28715edf..801989ca25 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuij github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= @@ -943,8 +945,8 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= -nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/provider/socket.go b/provider/socket.go index f66171cda8..d9ccb1902c 100644 --- a/provider/socket.go +++ b/provider/socket.go @@ -8,12 +8,12 @@ import ( "sync" "time" + "github.com/coder/websocket" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/provider/pipeline" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" - "nhooyr.io/websocket" ) const retryDelay = 5 * time.Second diff --git a/provider/socket_test.go b/provider/socket_test.go index 09f63180ca..757d72e4f0 100644 --- a/provider/socket_test.go +++ b/provider/socket_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + "github.com/coder/websocket" "github.com/stretchr/testify/require" - "nhooyr.io/websocket" ) func TestSocketProvider(t *testing.T) { diff --git a/server/socket.go b/server/socket.go index 7ecf3a4a21..cf48617226 100644 --- a/server/socket.go +++ b/server/socket.go @@ -8,8 +8,8 @@ import ( "sync" "time" + "github.com/coder/websocket" "github.com/evcc-io/evcc/util" - "nhooyr.io/websocket" ) const ( From 68f9e06b4a4255eff7c4f0a59b41894775d066c5 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 11 Aug 2024 15:04:46 +0200 Subject: [PATCH 16/35] chore: fix build --- charger/nrggen2.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/charger/nrggen2.go b/charger/nrggen2.go index 1c3c0ccde4..5afcc25094 100644 --- a/charger/nrggen2.go +++ b/charger/nrggen2.go @@ -71,10 +71,7 @@ func NewNRGKickGen2FromConfig(other map[string]interface{}) (api.Charger, error) return nil, err } - var ( - phasesS func(int) error - ) - + var phasesS func(int) error if cc.Phases1p3p { // user could have an adapter plug which doesn't support 3 phases if b, err := nrg.conn.ReadHoldingRegisters(nrgKickGen2MaxPhases, 1); err == nil { @@ -167,7 +164,7 @@ func (nrg *NRGKickGen2) Status() (api.ChargeStatus, error) { if err != nil { return api.StatusNone, err } - return api.StatusF, fmt.Errorf("%d", error) + return api.StatusF, fmt.Errorf("%d", binary.BigEndian.Uint16(b)) case 7: return api.StatusB, nil default: From 784c554f719e6b4fe45c2209a452cae9f332a617 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 11 Aug 2024 21:47:38 +0200 Subject: [PATCH 17/35] chore: fix whitespace --- templates/definition/charger/nrggen2.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/definition/charger/nrggen2.yaml b/templates/definition/charger/nrggen2.yaml index aa5ac0ac68..6b0b2538d1 100644 --- a/templates/definition/charger/nrggen2.yaml +++ b/templates/definition/charger/nrggen2.yaml @@ -23,4 +23,3 @@ render: | {{- if ne .phases1p3p "false" }} phases1p3p: true {{- end }} - From 8958b6ecd7d29efe3a87dd6b3de0af1746d4ff8c Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 11 Aug 2024 21:49:34 +0200 Subject: [PATCH 18/35] Fix case-insensitively merging template maps (#15351) --- util/templates/merge.go | 55 ++++++++++++++++++++++++++++++++++++ util/templates/merge_test.go | 32 +++++++++++++++++++++ util/templates/template.go | 5 ++-- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 util/templates/merge.go create mode 100644 util/templates/merge_test.go diff --git a/util/templates/merge.go b/util/templates/merge.go new file mode 100644 index 0000000000..fa9efdfc49 --- /dev/null +++ b/util/templates/merge.go @@ -0,0 +1,55 @@ +package templates + +import ( + "reflect" + "strings" +) + +// https://github.com/peterbourgon/mergemap + +const mergeMaxDepth = 100 + +var matchKey = strings.EqualFold + +// mergeMaps recursively merges other into target using matchKey for key comparison +func mergeMaps(other map[string]any, target map[string]any) error { + // return mergo.Map(&target, other, mergo.WithOverride) + // return util.DecodeOther(other, target) + merge(target, other, 0) + return nil +} + +func merge(dst, src map[string]any, depth int) map[string]any { + if depth > mergeMaxDepth { + panic("too deep!") + } + for key, srcVal := range src { + for k := range dst { + if matchKey(k, key) { + // overwrite key + key = k + + srcMap, srcMapOk := mapify(srcVal) + dstMap, dstMapOk := mapify(dst[k]) + if srcMapOk && dstMapOk { + srcVal = merge(dstMap, srcMap, depth+1) + } + break + } + } + dst[key] = srcVal + } + return dst +} + +func mapify(i any) (map[string]any, bool) { + value := reflect.ValueOf(i) + if value.Kind() == reflect.Map { + m := map[string]any{} + for _, k := range value.MapKeys() { + m[k.String()] = value.MapIndex(k).Interface() + } + return m, true + } + return map[string]any{}, false +} diff --git a/util/templates/merge_test.go b/util/templates/merge_test.go new file mode 100644 index 0000000000..b8e878872a --- /dev/null +++ b/util/templates/merge_test.go @@ -0,0 +1,32 @@ +package templates + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMergeMaps(t *testing.T) { + target := map[string]any{ + "foo": "bar", + "nested": map[string]any{ + "bar": "baz", + }, + } + other := map[string]any{ + "Foo": 1, + "Nested": map[string]any{ + "Bar": 2, + }, + "baz": 3, + } + + require.NoError(t, mergeMaps(other, target)) + require.Equal(t, map[string]any{ + "foo": 1, + "nested": map[string]any{ + "bar": 2, + }, + "baz": 3, + }, target) +} diff --git a/util/templates/template.go b/util/templates/template.go index b982a8f291..c2e1d09b9a 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -9,7 +9,6 @@ import ( "strings" "text/template" - "github.com/evcc-io/evcc/util" "github.com/go-sprout/sprout" ) @@ -267,9 +266,9 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, lang str } // RenderResult renders the result template to instantiate the proxy -func (t *Template) RenderResult(renderMode int, other map[string]interface{}) ([]byte, map[string]interface{}, error) { +func (t *Template) RenderResult(renderMode int, other map[string]any) ([]byte, map[string]any, error) { values := t.Defaults(renderMode) - if err := util.DecodeOther(other, &values); err != nil { + if err := mergeMaps(other, values); err != nil { return nil, values, err } From 9fa15d9a7e84b93c5763b0209eee7e3d572d72e0 Mon Sep 17 00:00:00 2001 From: carygravel <4869402+carygravel@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:53:20 +0000 Subject: [PATCH 19/35] cli/checkconfig: document limitations (#15348) --- cmd/check_config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/check_config.go b/cmd/check_config.go index 6ba015ad14..088ae24649 100644 --- a/cmd/check_config.go +++ b/cmd/check_config.go @@ -11,7 +11,10 @@ import ( var checkconfig = &cobra.Command{ Use: "checkconfig", Short: "Check config file for errors", - Run: runConfigCheck, + Long: `Check the (specified or default) config file for errors. Note that + checkconfig only checks the config file for parsing errors and does + not check that individual device configurations are valid.`, + Run: runConfigCheck, } func init() { From d4ecb4b2db8c9a035ca739a0361ff3e94edc3702 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Mon, 12 Aug 2024 08:09:42 +0200 Subject: [PATCH 20/35] Log UI: select multiple areas (#15338) --- assets/js/api.js | 16 ++++ assets/js/components/MultiSelect.vue | 126 +++++++++++++++++++++++++++ assets/js/router.js | 7 ++ assets/js/views/Log.vue | 93 ++++++++++++++------ i18n/de.toml | 2 + i18n/en.toml | 2 + 6 files changed, 218 insertions(+), 28 deletions(-) create mode 100644 assets/js/components/MultiSelect.vue diff --git a/assets/js/api.js b/assets/js/api.js index 45859733fc..cd7fbd4d9e 100644 --- a/assets/js/api.js +++ b/assets/js/api.js @@ -5,11 +5,27 @@ const { protocol, hostname, port, pathname } = window.location; const base = protocol + "//" + hostname + (port ? ":" + port : "") + pathname; +// override the way axios serializes arrays in query parameters (a=1&a=2&a=3 instead of a[]=1&a[]=2&a[]=3) +function customParamsSerializer(params) { + const queryString = Object.keys(params) + .filter((key) => params[key] !== null) + .map((key) => { + const value = params[key]; + if (Array.isArray(value)) { + return value.map((v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join("&"); + } + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + }) + .join("&"); + return queryString; +} + const api = axios.create({ baseURL: base + "api/", headers: { Accept: "application/json", }, + paramsSerializer: customParamsSerializer, }); // global error handling diff --git a/assets/js/components/MultiSelect.vue b/assets/js/components/MultiSelect.vue new file mode 100644 index 0000000000..e7a3124273 --- /dev/null +++ b/assets/js/components/MultiSelect.vue @@ -0,0 +1,126 @@ + + + diff --git a/assets/js/router.js b/assets/js/router.js index b0b993f663..2bdaaab6e1 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -56,6 +56,13 @@ export default function setupRouter(i18n) { path: "/log", component: () => import("./views/Log.vue"), beforeEnter: ensureAuth, + props: (route) => { + const { areas, level } = route.query; + return { + areas: areas ? areas.split(",") : undefined, + level, + }; + }, }, ], }); diff --git a/assets/js/views/Log.vue b/assets/js/views/Log.vue index a22632570b..5e7a911a8e 100644 --- a/assets/js/views/Log.vue +++ b/assets/js/views/Log.vue @@ -47,29 +47,26 @@
- + {{ areasLabel }} +

@@ -112,14 +109,15 @@ import "@h2d2/shopicons/es/regular/download"; import TopHeader from "../components/TopHeader.vue"; import Play from "../components/MaterialIcon/Play.vue"; import Record from "../components/MaterialIcon/Record.vue"; +import MultiSelect from "../components/MultiSelect.vue"; import api from "../api"; import store from "../store"; -const LEVELS = ["FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"]; -const DEFAULT_LEVEL = "DEBUG"; +const LEVELS = ["fatal", "error", "warn", "info", "debug", "trace"]; +const DEFAULT_LEVEL = "debug"; const DEFAULT_COUNT = 1000; -const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.join("|")})`); +const levelMatcher = new RegExp(`\\[.*?\\] (${LEVELS.map((l) => l.toUpperCase()).join("|")})`); export default { name: "Log", @@ -127,14 +125,17 @@ export default { TopHeader, Play, Record, + MultiSelect, + }, + props: { + areas: { type: Array, default: () => [] }, + level: { type: String, default: DEFAULT_LEVEL }, }, data() { return { lines: [], - areas: [], + availableAreas: [], search: "", - level: DEFAULT_LEVEL, - area: "", timeout: null, levels: LEVELS, busy: false, @@ -168,6 +169,18 @@ export default { return { key, className, line }; }); }, + areaOptions() { + return this.availableAreas.map((area) => ({ name: area, value: area })); + }, + areasLabel() { + if (this.areas.length === 0) { + return this.$t("log.areas"); + } else if (this.areas.length === 1) { + return this.areas[0]; + } else { + return this.$t("log.nAreas", { count: this.areas.length }); + } + }, showMoreButton() { return this.lines.length === DEFAULT_COUNT; }, @@ -179,9 +192,9 @@ export default { if (this.level) { params.append("level", this.level); } - if (this.area) { - params.append("area", this.area); - } + this.areas.forEach((area) => { + params.append("area", area); + }); params.append("format", "txt"); return `./api/system/log?${params.toString()}`; }, @@ -189,6 +202,14 @@ export default { return this.timeout !== null; }, }, + watch: { + selectedAreas() { + this.updateLogs(); + }, + level() { + this.updateLogs(); + }, + }, methods: { async updateLogs(showAll) { // prevent concurrent requests @@ -198,8 +219,8 @@ export default { this.busy = true; const response = await api.get("/system/log", { params: { - level: this.level?.toLocaleLowerCase() || null, - area: this.area || null, + level: this.level || null, + area: this.areas.length ? this.areas : null, count: showAll ? null : DEFAULT_COUNT, }, }); @@ -232,7 +253,7 @@ export default { async updateAreas() { try { const response = await api.get("/system/log/areas"); - this.areas = response.data?.result || []; + this.availableAreas = response.data?.result || []; } catch (e) { console.error(e); } @@ -260,6 +281,22 @@ export default { this.startInterval(); } }, + updateQuery({ level, areas }) { + let newLevel = level || this.level; + let newAreas = areas || this.areas; + // reset to default level + if (newLevel === DEFAULT_LEVEL) newLevel = undefined; + newAreas = newAreas.length ? newAreas.join(",") : undefined; + this.$router.push({ + query: { level: newLevel, areas: newAreas }, + }); + }, + changeLevel(event) { + this.updateQuery({ level: event.target.value }); + }, + changeAreas(areas) { + this.updateQuery({ areas }); + }, }, }; diff --git a/i18n/de.toml b/i18n/de.toml index 3af110a2b6..62b187c7b6 100644 --- a/i18n/de.toml +++ b/i18n/de.toml @@ -348,8 +348,10 @@ areaLabel = "Nach Bereich filtern" areas = "Alle Bereiche" download = "Komplettes Log herunterladen" levelLabel = "Nach Log-Level filtern" +nAreas = "{count} Bereiche" noResults = "Keine passenden Einträge gefunden." search = "Suchen" +selectAll = "alle wählen" showAll = "Alle Einträge anzeigen" title = "Logs" update = "Aktualisieren" diff --git a/i18n/en.toml b/i18n/en.toml index 655bbf33f6..5ca4eb1094 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -350,8 +350,10 @@ areaLabel = "Filter by area" areas = "All areas" download = "Download complete log" levelLabel = "Filter by log level" +nAreas = "{count} areas" noResults = "No matching log entries." search = "Search" +selectAll = "select all" showAll = "Show all entries" title = "Logs" update = "Auto update" From bfa9901cb8782d3a7ffdd74a44a096d6a3a0a0a9 Mon Sep 17 00:00:00 2001 From: andig Date: Mon, 12 Aug 2024 13:35:50 +0200 Subject: [PATCH 21/35] chore: allows dumping rendered templates using EVCC_TEMPLATE_RENDER=