From 9b48d6637f79a908a9faad789b1e6edf561f1261 Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Fri, 3 Jan 2025 09:42:11 +0100 Subject: [PATCH 01/25] =?UTF-8?q?Weidm=C3=BCller:=20fix=20register=20usage?= =?UTF-8?q?=20and=20check=20for=20meter=20(#18019)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "charger/weidm\303\274ller.go" | 67 +++++++++++++++++------ "charger/weidm\303\274ller_decorators.go" | 35 ++++++++++++ 2 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 "charger/weidm\303\274ller_decorators.go" diff --git "a/charger/weidm\303\274ller.go" "b/charger/weidm\303\274ller.go" index efebcd1171..9afbae9195 100644 --- "a/charger/weidm\303\274ller.go" +++ "b/charger/weidm\303\274ller.go" @@ -2,7 +2,7 @@ package charger // LICENSE -// Copyright (c) 2023 premultiply +// Copyright (c) 2023-2025 premultiply // This module is NOT covered by the MIT license. All rights reserved. @@ -18,8 +18,10 @@ package charger // SOFTWARE. import ( + "context" "encoding/binary" "fmt" + "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" @@ -36,23 +38,27 @@ type Weidmüller struct { } const ( - wmRegCarStatus = 301 // GD_ID_EVCC_CAR_STATE CHAR - wmRegEvccStatus = 302 // GD_ID_EVCC_EVSE_STATE UINT16 - wmRegPhases = 318 // GD_ID_EVCC_PHASES_LLM UINT16 - wmRegVoltages = 400 // GD_ID_CM_VOLTAGE_PHASE UINT32 - wmRegCurrents = 406 // GD_ID_CM_CURRENT_PHASE UINT32 - wmRegActivePower = 418 // GD_ID_CM_ACTIVE_POWER UINT32 - wmRegTotalEnergy = 457 // GD_ID_CM_CONSUMED_ENERGY_TOTAL_WH UINT64 - wmRegCurrentLimit = 702 // GD_ID_AUT_USER_CURRENT_LIMIT UINT16 - wmRegCardId = 1000 // GD_ID_RFID_TAG_UID CHAR[21] + wmRegCarStatus = 301 // GD_ID_EVCC_CAR_STATE CHAR + wmRegEvccStatus = 302 // GD_ID_EVCC_EVSE_STATE UINT16 + wmRegPhases = 318 // GD_ID_EVCC_PHASES_LLM UINT16 + wmRegVoltages = 400 // GD_ID_CM_VOLTAGE_PHASE UINT32 + wmRegCurrents = 406 // GD_ID_CM_CURRENT_PHASE UINT32 + wmRegActivePower = 418 // GD_ID_CM_ACTIVE_POWER UINT32 + wmRegTotalEnergy = 457 // GD_ID_CM_CONSUMED_ENERGY_TOTAL_WH UINT64 + wmRegCardId = 1000 // GD_ID_RFID_TAG_UID CHAR[21] + wmRegTimeout = 11050 // GD_ID_LCM_TIMEOUT UINT32 + wmRegCurrentLimit = 11052 // GD_ID_LCM_ACTUAL_CURRENT_LIMIT UINT16 (A) + + wmHeartbeatInterval = 5 * time.Second + wmTimeout = 65535 // ms ) func init() { - registry.Add("weidmüller", NewWeidmüllerFromConfig) + registry.AddCtx("weidmüller", NewWeidmüllerFromConfig) } // NewWeidmüllerFromConfig creates a Weidmüller charger from generic config -func NewWeidmüllerFromConfig(other map[string]interface{}) (api.Charger, error) { +func NewWeidmüllerFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { cc := modbus.TcpSettings{ ID: 255, } @@ -61,11 +67,13 @@ func NewWeidmüllerFromConfig(other map[string]interface{}) (api.Charger, error) return nil, err } - return NewWeidmüller(cc.URI, cc.ID) + return NewWeidmüller(ctx, cc.URI, cc.ID) } +//go:generate decorate -f decorateWeidmüller -b *Weidmüller -r api.Charger -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" + // NewWeidmüller creates Weidmüller charger -func NewWeidmüller(uri string, id uint8) (api.Charger, error) { +func NewWeidmüller(ctx context.Context, uri string, id uint8) (api.Charger, error) { conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id) if err != nil { return nil, err @@ -93,7 +101,32 @@ func NewWeidmüller(uri string, id uint8) (api.Charger, error) { wb.curr = curr } - return wb, err + // failsafe + go wb.heartbeat(ctx, wmHeartbeatInterval) + + // check presence of energy meter + if b, err := wb.conn.ReadHoldingRegisters(wmRegTotalEnergy, 2); err == nil && binary.BigEndian.Uint32(b) > 0 { + return decorateWeidmüller(wb, wb.totalEnergy), nil + } + + return wb, nil +} + +func (wb *Weidmüller) heartbeat(ctx context.Context, timeout time.Duration) { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, wmTimeout) + + for tick := time.Tick(timeout); ; { + select { + case <-tick: + case <-ctx.Done(): + return + } + + if _, err := wb.conn.WriteMultipleRegisters(wmRegTimeout, 2, b); err != nil { + wb.log.ERROR.Println("heartbeat:", err) + } + } } func (wb *Weidmüller) setCurrent(current uint16) error { @@ -184,10 +217,8 @@ func (wb *Weidmüller) CurrentPower() (float64, error) { return float64(encoding.Uint32LswFirst(b)) / 1e3, err } -var _ api.MeterEnergy = (*Weidmüller)(nil) - // TotalEnergy implements the api.MeterEnergy interface -func (wb *Weidmüller) TotalEnergy() (float64, error) { +func (wb *Weidmüller) totalEnergy() (float64, error) { b, err := wb.conn.ReadHoldingRegisters(wmRegTotalEnergy, 2) if err != nil { return 0, err diff --git "a/charger/weidm\303\274ller_decorators.go" "b/charger/weidm\303\274ller_decorators.go" new file mode 100644 index 0000000000..1df7545327 --- /dev/null +++ "b/charger/weidm\303\274ller_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 decorateWeidmüller(base *Weidmüller, meterEnergy func() (float64, error)) api.Charger { + switch { + case meterEnergy == nil: + return base + + case meterEnergy != nil: + return &struct { + *Weidmüller + api.MeterEnergy + }{ + Weidmüller: base, + MeterEnergy: &decorateWeidmüllerMeterEnergyImpl{ + meterEnergy: meterEnergy, + }, + } + } + + return nil +} + +type decorateWeidmüllerMeterEnergyImpl struct { + meterEnergy func() (float64, error) +} + +func (impl *decorateWeidmüllerMeterEnergyImpl) TotalEnergy() (float64, error) { + return impl.meterEnergy() +} From ef2993507b1fb436eaa4d9a918ffa3de8fc7cadc Mon Sep 17 00:00:00 2001 From: Twan <1204923+tcoenraad@users.noreply.github.com> Date: Fri, 3 Jan 2025 09:43:48 +0100 Subject: [PATCH 02/25] Add SAJ R5 template (#18014) --- templates/definition/meter/saj-r5.yaml | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 templates/definition/meter/saj-r5.yaml diff --git a/templates/definition/meter/saj-r5.yaml b/templates/definition/meter/saj-r5.yaml new file mode 100644 index 0000000000..c434a851a4 --- /dev/null +++ b/templates/definition/meter/saj-r5.yaml @@ -0,0 +1,31 @@ +template: saj-r5 +products: + - brand: SAJ + description: + generic: R5 Series Solar Inverter +params: + - name: usage + choice: ["pv"] + - name: modbus + choice: ["rs485"] + baudrate: 9600 + comset: 8N1 +render: | + type: custom + {{- if eq .usage "pv" }} + power: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 275 # 0x0113 Active power of inverter total output + type: holding + decode: uint16 + energy: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: 305 # 0x0131 Total Energy + type: holding + decode: uint32 + scale: 0.01 + {{- end }} From e708c0ba9e48a7cbf1b41caa18806e3874fe0ae8 Mon Sep 17 00:00:00 2001 From: deadrabbit87 Date: Fri, 3 Jan 2025 12:46:06 +0100 Subject: [PATCH 03/25] Add Weishaupt SG Ready charger (#18026) --- .../definition/charger/weishaupt-wpm.yaml | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 templates/definition/charger/weishaupt-wpm.yaml diff --git a/templates/definition/charger/weishaupt-wpm.yaml b/templates/definition/charger/weishaupt-wpm.yaml new file mode 100644 index 0000000000..1a45bb1ae0 --- /dev/null +++ b/templates/definition/charger/weishaupt-wpm.yaml @@ -0,0 +1,113 @@ +template: weishaupt-wpm +products: + - brand: Weishaupt + description: + generic: WPM (SG Ready) +group: heating +# requirements: +# evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] + - name: tempsource + type: choice + choice: ["", "warmwater", "buffer"] + description: + de: "Temperaturquelle" + en: "Temperature source" +render: | + type: sgready + getmode: + source: map + values: + 8: 1 # ABTAUEN + 10: 3 # EVU_SPERRE + 14: 2 # ERHOEHTER_BETRIEB + 18: 3 # FROSTSCHUTZ + 19: 1 # HEIZBETRIEB + get: + source: modbus + {{- include "modbus" . | indent 4 }} + register: + address: 30006 + type: input + encoding: uint16 + setmode: + source: switch + switch: + - case: 1 # normal + set: + source: sequence + set: + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 45101 # 4001 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 45102 # 4002 + type: writesingle + encoding: uint16 + - case: 2 # boost + set: + source: sequence + set: + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 45101 + type: writesingle + encoding: uint16 + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 45102 + type: writesingle + encoding: uint16 + - case: 3 # dimm + set: + source: sequence + set: + - source: const + value: 0 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 45101 + type: writesingle + encoding: uint16 + - source: const + value: 1 + set: + source: modbus + {{- include "modbus" . | indent 10 }} + register: + address: 45102 + type: writesingle + encoding: uint16 + {{- if .tempsource }} + temp: + source: modbus + {{- include "modbus" . | indent 2 }} + register: + address: {{ if eq .tempsource "warmwater" -}} 32102 {{ else }} 33104 {{- end }} # 32102 = WW; 33104 Vorlauf + type: input + encoding: int16 + scale: 0.1 + {{- end }} From 55cdcccee3e5779e6fbaeb14fda1ab81851b50cb Mon Sep 17 00:00:00 2001 From: fscherwi Date: Fri, 3 Jan 2025 13:05:27 +0100 Subject: [PATCH 04/25] docs: fix bmw hcaptcha link (#17952) --- templates/definition/vehicle/bmw.yaml | 4 ++-- templates/definition/vehicle/mini.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/definition/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml index b4ac04d332..be57f39099 100644 --- a/templates/definition/vehicle/bmw.yaml +++ b/templates/definition/vehicle/bmw.yaml @@ -4,9 +4,9 @@ products: requirements: description: de: | - Benötigt `hcaptcha` Token. Dieses muss einmalig unter https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten. + Benötigt `hcaptcha` Token. Dieses muss einmalig unter [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html) generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten. en: | - Requires `hcaptcha` token. This must be generated once at https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html. The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation. + Requires `hcaptcha` token. This must be generated once at [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html). The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation. params: - preset: vehicle-base - name: vin diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml index 56f28c6589..8611f8713f 100644 --- a/templates/definition/vehicle/mini.yaml +++ b/templates/definition/vehicle/mini.yaml @@ -4,9 +4,9 @@ products: requirements: description: de: | - Benötigt `hcaptcha` Token. Dieses muss einmalig unter https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten. + Benötigt `hcaptcha` Token. Dieses muss einmalig unter [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html) generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten. en: | - Requires `hcaptcha` token. This must be generated once at https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html. The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation. + Requires `hcaptcha` token. This must be generated once at [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html). The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation. params: - preset: vehicle-base - name: vin From b7a843d087661eb0bd5d7139ba73fb32ab4c042a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 3 Jan 2025 14:15:14 +0100 Subject: [PATCH 05/25] Translations update from Hosted Weblate (#17958) * Translated using Weblate (Bulgarian) Currently translated at 92.9% (552 of 594 strings) Translated using Weblate (Bulgarian) Currently translated at 79.1% (470 of 594 strings) Translated using Weblate (Bulgarian) Currently translated at 49.6% (295 of 594 strings) Translated using Weblate (Bulgarian) Currently translated at 41.0% (244 of 594 strings) Translated using Weblate (Bulgarian) Currently translated at 37.3% (222 of 594 strings) Translated using Weblate (Bulgarian) Currently translated at 19.3% (115 of 594 strings) Co-authored-by: Tim Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/bg/ Translation: evcc/evcc * Translated using Weblate (Croatian) Currently translated at 100.0% (594 of 594 strings) Co-authored-by: Ante Karamatic Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/hr/ Translation: evcc/evcc * Translated using Weblate (Turkish) Currently translated at 100.0% (594 of 594 strings) Co-authored-by: aerucu Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/tr/ Translation: evcc/evcc * fix toml --------- Co-authored-by: Tim Co-authored-by: Ante Karamatic Co-authored-by: aerucu Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> --- i18n/bg.toml | 530 ++++++++++++++++++++++++++++++++++++++++++++++++++- i18n/hr.toml | 1 + i18n/tr.toml | 3 +- 3 files changed, 526 insertions(+), 8 deletions(-) diff --git a/i18n/bg.toml b/i18n/bg.toml index 8f5178616e..add79734b8 100644 --- a/i18n/bg.toml +++ b/i18n/bg.toml @@ -1,20 +1,25 @@ [batterySettings] batteryLevel = "Батерия %" capacity = "{energy} от {total}" +control = "Управление на батерията" +discharge = "Предотврати разреждането в бърз режим и планираното зареждане" disclaimerHint = "Бележка:" disclaimerText = "Тези настройки важат само за режим на работа използвайки енергия от фотоволтаици. Зареждането ще бъде настроено спрямо тях" -legendBottomName = "приоритизиране на дом" -legendBottomSubline = "не се използва за зареждане" -legendMiddleName = "приоритизиране на кола" -legendMiddleSubline = "дом на заден план" +gridChargeTab = "Зареждане от мрежата" +legendBottomName = "Приоритизиране на зареждането на домашната батерия" +legendBottomSubline = "докато достигне {soc}." +legendMiddleName = "Приоритизиране на зареждането на превозното средство" +legendMiddleSubline = "когато домашната батерия е над {soc}." legendTitle = "Как да се използва енергията от фотоволтаиците?" legendTopAutostart = "автоматично стартиране" legendTopName = "зареждане използвайки батерия" -legendTopSubline = "без прекъсвания" -modalTitle = "Настройки на батерия" +legendTopSubline = "когато домашната батерия е над {soc}." +modalTitle = "Домашна батерия" +usageTab = "Използване на батерията" [batterySettings.bufferStart] -full = "при почти заредена батерия" +above = "когато над {soc}" +full = "при {soc}." low = "при донякъде заредена батерия" medium = "при наполовина заредена батерия" never = "само при наличие на излишък" @@ -22,62 +27,213 @@ never = "само при наличие на излишък" [config] [config.battery] +titleAdd = "Добави батерия" +titleEdit = "Редактиране на батерията" [config.circuits] +description = "Гарантира, че сумата от всички точки на натоварване, свързани към една верига, не надвишава конфигурираните граници на мощност и ток. Веригите могат да бъдат вложени, за да се изгради йерархия." +title = "Управление на натоварването" [config.control] +description = "Обикновено стойностите по подразбиране са наред. Променяйте ги само ако знаете какво правите." +descriptionInterval = "Цикъл на актуализация на контролния цикъл в секунди. Определя колко често evcc чете данни от измервателния уред, регулира мощността на зареждане и актуализира потребителския интерфейс. Кратките интервали (< 30s) могат да причинят осцилации и нежелано поведение." +descriptionResidualPower = "Премества работната точка на контролния цикъл. Ако имате домашна батерия, се препоръчва да зададете стойност от 100 W. По този начин батерията ще има лек приоритет пред използването на мрежата." +labelInterval = "Интервал на актуализация" +labelResidualPower = "Остатъчна мощност" +title = "Поведение на управление" [config.deviceValue] +broker = "Брокер" +bucket = "Кофа" +capacity = "Капацитет" +chargeStatus = "Статус" +chargeStatusA = "не е свързан" +chargeStatusB = "свързан" +chargeStatusC = "зареждане" +chargeStatusE = "няма мощност" +chargeStatusF = "грешка" +chargedEnergy = "Заредено" +co2 = "Мрежа CO₂" +configured = "Конфигуриран" +controllable = "Контролируем" +currency = "Валута" +current = "Електричеството" +currentRange = "Електричеството" +enabled = "Готов за зареждане" +energy = "Енергия" +feedinPrice = "Цена за изкупуване" +gridPrice = "Цена на мрежата" +hemsType = "Система" +no = "не" +odometer = "одометър" +org = "Организация" +phaseCurrents = "Текущ L1, L2, L3" +phasePowers = "Мощност L1, L2, L3" +phaseVoltages = "Напрежение L1, L2, L3" +power = "Мощност" +powerRange = "Мощност" +range = "Обхват" +soc = "Състояние на заряда" +socLimit = "Лимит" +temp = "Температура" +topic = "Тема" +url = "URL" +yes = "да" [config.eebus] +description = "Основна конфигурация за комуникация с други EEBus устройства." +title = "EEBus" [config.form] example = "Пример" optional = "незадължителен" [config.general] +cancel = "Отказ" +docsLink = "Виж документацията." +experimental = "Експериментален" +hideAdvancedSettings = "Скрий разширените настройки" +off = "изключено" +on = "включено" +password = "Парола" +readFromFile = "Прочети от файл" +remove = "Премахни" +save = "Запази" +showAdvancedSettings = "Покажи разширените настройки" +telemetry = "Телеметрия" +title = "Заглавие" [config.grid] +title = "Мрежов измервателен уред" +titleAdd = "Добави мрежов измервателен уред" +titleEdit = "Редактирай мрежов измервателен уред" [config.hems] +description = "Свържете evcc с друга система за управление на домашната енергия." +title = "„Система за управление на енергията в дома“" [config.influx] +description = "Записва данни за зареждане и други метрики в InfluxDB. Използвайте Grafana или други инструменти за визуализиране на данните." +descriptionToken = "Проверете документацията на InfluxDB, за да научите как да създадете такъв. https://docs.influxdata.com/influxdb/v2/admin/" +labelBucket = "Кофа" +labelCheckInsecure = "Разреши самоподписани сертификати" +labelDatabase = "База данни" +labelInsecure = "„Проверка на сертификат“" +labelOrg = "Организация" +labelPassword = "Парола" +labelToken = "API токен" +labelUrl = "URL" +labelUser = "Потребителско име" +title = "InfluxDB" +v1Support = "Нуждаете се от поддръжка за InfluxDB 1.x?" +v2Support = "Назад към InfluxDB 2.x" [config.main] +addLoadpoint = "Добави зарядна точка" +addPvBattery = "Добави соларен панел или батерия" addVehicle = "Добави автомобил" +configured = "Конфигуриран" edit = "редактирай" title = "Конфигурация" +unconfigured = "не е конфигуриран" vehicles = "Моите Автомобили" +yaml = "Конфигурирано в evcc.yaml. Не може да се редактира в потребителския интерфейс." [config.messaging] +description = "Получавайте съобщения за вашите сесии на зареждане." +title = "Notifications" [config.meter] +cancel = "Отказ" +delete = "Изтрий" +save = "Запази" +template = "Производител" +titleChoice = "Какво искате да добавите?" +validateSave = "Валидирай и запази" [config.modbusproxy] +description = "Позволете на няколко клиента да имат достъп до едно Modbus устройство." +title = "„Модбус прокси“" [config.mqtt] +authentication = "Автентикация" +description = "Свържете се с MQTT брокер, за да обменяте данни с други системи във вашата мрежа." +descriptionClientId = "Автор на съобщенията. Ако е празно, се използва `evcc-[rand]`." +descriptionTopic = "Оставете празно, за да деактивирате публикуването." +labelBroker = "Брокер" +labelCaCert = "Сървърен сертификат (CA)" +labelCheckInsecure = "Позволете самоподписани сертификати" +labelClientCert = "Клиентски сертификат" +labelClientId = "Клиентски идентификатор" +labelClientKey = "Клиентски ключ" +labelInsecure = "Валидиране на сертификат" +labelPassword = "Парола" +labelTopic = "Тема" +labelUser = "Потребителско име" +publishing = "Публикуване" +title = "MQTT" [config.network] +descriptionHost = "„Използвайте разширението .local, за да активирате mDNS. Това е от значение за разпознаване на мобилни приложения и някои зарядни станции OCPP.“" +descriptionPort = "Порт за уеб интерфейса и API. Ще трябва да актуализирате URL адреса на браузъра си, ако промените това." +descriptionSchema = "Влияе само на начина, по който се генерират URL адресите. Изборът на HTTPS няма да активира криптиране." +labelHost = "Хост име" +labelPort = "Порт" +labelSchema = "Схема" +title = "Мрежа" [config.options] [config.options.boolean] +no = "не" +yes = "да" [config.options.endianness] +big = "голям ендиан" +little = "„малко ендианско“" [config.options.schema] +http = "HTTP (некриптиран)" +https = "HTTPS (криптиран)" [config.pv] +titleAdd = "Добавяне на соларен метър" +titleEdit = "Редактиране на соларен метър" [config.section] +general = "Общи" +grid = "Мрежова връзка" +integrations = "Интеграции" +loadpoints = "Зарядни точки" +meter = "Солар и батерия" +system = "Система" +vehicles = "Превозни средства" [config.sponsor] +addToken = "Въведете спонсорски токен" +changeToken = "Промяна на спонсорския токен" +description = "Моделът за спонсорство ни помага да поддържаме проекта и устойчиво да изграждаме нови и вълнуващи функции. Като спонсор получавате достъп до всички реализации на зарядни устройства." +descriptionToken = "Получавате токена от {url}. Ние също предлагаме пробен токен за тестване." +error = "Спонсорският токен не е валиден." +labelToken = "Спонсорски токен" +title = "Спонсорство" [config.system] +logs = "Логове" +restart = "Рестартиране" +restartRequiredDescription = "Моля, рестартирайте, за да видите ефекта." +restartRequiredMessage = "Конфигурацията е променена." +restartingDescription = "Моля, изчакайте..." +restartingMessage = "Рестартиране на evcc." [config.tariffs] +description = "Определете вашите енергийни тарифи, за да изчислите разходите за вашите сесии на зареждане." +title = "Тарифи" [config.title] +description = "Показва се на главния екран и в раздела на браузъра." +label = "Заглавие" +title = "Редактиране на заглавие" [config.validation] failed = "неуспешен" @@ -115,15 +271,22 @@ powerSub2 = "зарежда..." tabTitle = "Общност" [footer.savings] +co2Saved = "{value} запазени" +co2Title = "CO₂ емисии" +configurePriceCo2 = "Научете как да конфигурирате данните за цените и CO₂." footerLong = "{percent} енергия от слънцето" footerShort = "{percent} слънчева енергия" modalTitle = "Информация за зареждането" +moneySaved = "{value} запазени" percentGrid = "{grid} кВч от мрежата" percentSelf = "{self} кВч от фотоволтаици" percentTitle = "Енергия от фотоволтаици" +periodLabel = "Период:" priceFeedIn = "{feedInPrice} feed-in" priceGrid = "{gridPrice} мрежа" priceTitle = "Цена" +referenceGrid = "Мрежа" +referenceLabel = "Референтни данни:" savingsComparedToGrid = "сравнено с мрежата" savingsTitle = "Спестявате" savingsTotalEnergy = "{total} kWh заредени" @@ -131,9 +294,14 @@ since = "от {since}" tabTitle = "Моите данни" [footer.savings.period] +30d = "последните 30 дни" +365d = "последните 365 дни" +thisYear = "тази година" +total = "всички времена" [footer.sponsor] becomeSponsor = "Станете Спонсор" +becomeSponsorExtended = "Подкрепете ни директно, за да получите стикери." confetti = "Готови ли сте за конфети?" confettiPromise = "Получавате стикери и дигитални конфети" sticker = "... или evcc стикери?" @@ -141,6 +309,10 @@ supportUs = "Нашата мисия е да направим слънчеват thanks = "Благодарим Ви, {sponsor}! С ваша помощ продължаваме да развиваме evcc." titleNoSponsor = "Подкрепете ни" titleSponsor = "Вие сте спонсор" +titleTrial = "Режим на проба" +titleVictron = "Спонсорирано от Victron Energy" +trial = "Вие сте в пробен режим и можете да използвате всички функции. Моля, обмислете възможността да подкрепите проекта." +victron = "Вие използвате evcc на хардуера на Victron Energy и имате достъп до всички функции." [footer.telemetry] optIn = "Искам да споделям моите данни." @@ -161,96 +333,440 @@ modalUpdateStarted = "Стартирай новата версия на evcc..." modalUpdateStatusStart = "Инсталацията започна:" [header] +about = "Относно" +blog = "Блог" +docs = "Документация" +github = "GitHub" +login = "Автомобил-влизания в системата" +logout = "Изход" +nativeSettings = "Промяна на сървъра" +needHelp = "Нуждаете се от помощ?" +sessions = "Сесии за зареждане" [help] +discussionsButton = "GitHub дискусии" +documentationButton = "Документация" +issueButton = "Докладвайте за грешка" +issueDescription = "Открихте ли странно или неправилно поведение?" +logsButton = "Преглед на дневниците" +logsDescription = "Проверете дневниците за грешки." +modalTitle = "Нуждаете се от помощ?" +primaryActions = "Нещо не работи както трябва? Това са добри места, където можете да получите помощ." +restartButton = "Рестартиране" +restartDescription = "Опитахте ли да го изключите и включите отново?" +secondaryActions = "Все още не можете да решите проблема си? Ето някои по-сериозни опции." [help.restart] +cancel = "Отказ" +confirm = "Да, рестартирайте!" +description = "При нормални обстоятелства рестартирането не би трябвало да е необходимо. Моля, обмислете подаването на сигнал за грешка, ако трябва редовно да рестартирате evcc." +disclaimer = "Забележка: evcc ще се прекрати и ще разчита на операционната система за рестартиране на услугата." +modalTitle = "Сигурни ли сте, че искате да рестартирате?" [log] +areaLabel = "Филтриране по област" +areas = "Всички области" +download = "Изтеглете пълния дневник" +levelLabel = "Филтриране по ниво на дневника" +nAreas = "{count} области" +noResults = "Няма съвпадащи записи в дневника." +search = "Търсене" +selectAll = "Изберете всички" +showAll = "Показване на всички записи" +title = "Дневници" +update = "Автоматично обновяване" [loginModal] +cancel = "Отказ" +error = "Входът не бе успешен: " +iframeHint = "Отворете evcc в нов раздел." +iframeIssue = "Вашата парола е правилна, но изглежда, че браузърът ви е загубил бисквитката за удостоверяване. Това може да се случи, ако стартирате evcc в iframe чрез HTTP." +invalid = "Паролата е невалидна." +login = "Вход" +password = "Парола" +reset = "Нулиране на паролата?" +title = "Удостоверяване" [main] +vehicles = "Паркиране" [main.chargingPlan] +active = "Активен" +addRepeatingPlan = "Добавяне на повтарящ се план" +arrivalTab = "Пристигане" +day = "Ден" +departureTab = "Заминаване" +goal = "Цел на зареждане" +modalTitle = "План за зареждане" +none = "няма" +planNumber = "План {number}" +remove = "Премахване" +repeating = "повтарящ се" +repeatingPlans = "Повтарящи се планове" +selectAll = "Изберете всички" +time = "Време" +title = "План" +titleMinSoc = "Минимално зареждане" +titleTargetCharge = "Заминаване" +unsavedChanges = "Има незаписани промени. Приложи сега?" +update = "Приложи" +weekdays = "Дни" [main.energyflow] +battery = "Батерия" +batteryCharge = "Зареждане на батерията" +batteryDischarge = "Разреждане на батерията" +batteryHold = "Батерия (заключена)" +batteryTooltip = "{energy} от {total} ({soc})" +cheapBatteryGridCharge = "евтина мрежова енергия" +cleanBatteryGridCharge = "чиста мрежова енергия" +gridImport = "Използвана енергия от мрежата" +homePower = "Потребление" +loadpoints = "Зарядно устройство | Зарядно устройство | {count} зарядни устройства" +noEnergy = "Няма данни от измервателния уред" +pvExport = "Енергия, която се подава в електрическата мрежа" +pvProduction = "Производство" +selfConsumption = "Самопотребление" [main.heatingStatus] +charging = "Загряване…" +waitForVehicle = "Готово. Чака се нагревателят …" [main.loadpoint] +avgPrice = "⌀ Цена" +charged = "Заредено" +co2 = "⌀ CO₂" +duration = "Продължителност на зареждането" +fallbackName = "Зарядна точка" +power = "Мощност" +price = "Цена" +remaining = "Оставащо време" +remoteDisabledHard = "{source}: изключено" +remoteDisabledSoft = "{source}: изключено адаптивното соларно зареждане" +solar = "Соларен" [main.loadpointSettings] +batteryUsage = "Домашна батерия" +currents = "Заряден ток" +default = "по подразбиране" +disclaimerHint = "Забележка:" +onlyForSocBasedCharging = "Тези опции са налични само за превозни средства с известен заряд." +smartCostCheap = "Евтино зареждане от мрежата" +smartCostClean = "Чисто зареждане от мрежата" +title = "Настройки {0}" +vehicle = "Превозно средство" [main.loadpointSettings.batteryBoost] +description = "Бързо зареждане от домашната батерия" +label = "Батериен бууст" +mode = "Налично само в соларен и мин+соларен режим" +once = "Буустът е активен за тази сесия на зареждане" [main.loadpointSettings.limitSoc] +description = "Лимит на зареждане, който се използва, когато това превозно средство е свързано" +label = "Лимит по подразбиране" [main.loadpointSettings.maxCurrent] +label = "Макс. ток" [main.loadpointSettings.minCurrent] +label = "Мин. ток" [main.loadpointSettings.minSoc] +description = "Превозното средство се зарежда „бързо“ до {0} в соларен режим. След това продължава със соларен излишък. Полезно за осигуряване на минимален пробег дори в по-тъмни дни." +label = "Мин. заряд %" [main.loadpointSettings.phasesConfigured] +label = "Фази" +no1p3pSupport = "Как е свързана вашата зарядна станция?" +phases_0 = "Автоматично превключване" +phases_1 = "1 фаза" +phases_1_hint = "({min} до {max})" +phases_3 = "3 фази" +phases_3_hint = "({min} до {max})" [main.mode] +minpv = "Мин+Солар" +now = "Бързо" +off = "Изкл." +pv = "Солар" +smart = "Смарт" [main.provider] +login = "Вход" +logout = "Изход" [main.targetCharge] +activate = "Активиране" +co2Limit = "Лимит на CO₂ от {co2}" +costLimitIgnore = "Конфигурираният {limit} ще бъде игнориран през този период." +currentPlan = "Активен план" +descriptionEnergy = "До кога трябва да бъде заредена {targetEnergy} в превозното средство?" +descriptionSoc = "Кога трябва да бъде заредено превозното средство до {targetSoc}?" +inactiveLabel = "Целево време" +nextPlan = "Следващ план" +notReachableInTime = "Целта ще бъде постигната {overrun} по-късно." +onlyInPvMode = "Планът за зареждане работи само в соларен режим." +planDuration = "Време за зареждане" +planPeriodLabel = "Период" +planPeriodValue = "{start} до {end}" +planUnknown = "Все още не е известно" +preview = "Преглед на плана" +priceLimit = "Лимит на цената от {price}" +remove = "Премахване" +setTargetTime = "няма" +targetIsAboveLimit = "Конфигурираният лимит за зареждане от {limit} ще бъде игнориран през този период." +targetIsAboveVehicleLimit = "Лимитът на превозното средство е под целта за зареждане." +targetIsInThePast = "Изберете време в бъдещето, Марти." +targetIsTooFarInTheFuture = "Ще коригираме плана веднага щом научим повече за бъдещето." +title = "Целево време" +today = "днес" +tomorrow = "утре" +update = "Обновяване" +vehicleCapacityDocs = "Научете как да го конфигурирате." +vehicleCapacityRequired = "Капацитетът на батерията на превозното средство е необходим за изчисляване на времето за зареждане." [main.targetChargePlan] +chargeDuration = "Време за зареждане" +co2Label = "Емисия на CO₂ ⌀" +priceLabel = "Цена на енергията" +timeRange = "{day} {range} ч" +unknownPrice = "Все още не е известно" [main.targetEnergy] +label = "Лимит" +noLimit = "няма" [main.vehicle] +addVehicle = "Добавяне на превозно средство" +changeVehicle = "Промяна на превозното средство" +detectionActive = "Откриване на превозно средство…" +fallbackName = "Превозно средство" +moreActions = "Още действия" +none = "Няма превозно средство" +notReachable = "Превозното средство не беше достъпно. Опитайте да рестартирате evcc." +targetSoc = "Лимит" +temp = "Температура" +tempLimit = "Целева температура" +unknown = "Гостуващо превозно средство" +vehicleSoc = "Зареждане" [main.vehicleSoc] +charging = "зареждане" +connected = "свързан" +disconnected = "разединен" +ready = "готов" +vehicleLimit = "Лимит на превозното средство: {soc}" [main.vehicleStatus] +awaitingAuthorization = "Изчакване на разрешение." +batteryBoost = "Активно ускоряване на батерията." +charging = "Зареждане…" +cheapEnergyCharging = "Налична е евтина енергия." +cheapEnergyNextStart = "Евтината енергия в {duration}" +cheapEnergySet = "Лимитът на цената е зададен." +cleanEnergyCharging = "Налична е чиста енергия." +cleanEnergyNextStart = "Чиста енергия в {продължителност}." +cleanEnergySet = "Лимитът на CO₂ е зададен." +climating = "Открито е предварително кондициониране." +connected = "Превозното средство е свързано" +disconnectRequired = "Процесът е прекратен. Моля, свържете се отново." +disconnected = "Разединен." +finished = "Завършен." +minCharge = "Минимално зареждане до {soc}." +pvDisable = "Недостатъчен излишък. Скоро ще бъде пауза." +pvEnable = "Наличен излишък. Скоро ще започне." +scale1p = "Скоро ще се намали до еднофазно зареждане." +scale3p = "Скоро ще се увеличи до трифазно зареждане." +targetChargeActive = "Планът за зареждане е активен. Очаквано завършване след {duration}." +targetChargePlanned = "Планът за таксуване започва в {duration}" +targetChargeWaitForVehicle = "Планът за зареждане е готов. Очаква се превозното средство…" +unknown = "" +vehicleLimit = "Лимит на превозното средство." +vehicleLimitReached = "Достигнат е лимитът на превозното средство." +waitForVehicle = "Готов. Очакване на превозното средство…" +welcome = "Кратко първоначално зареждане за потвърждаване на връзката." [notifications] +dismissAll = "Отхвърли всички" +logs = "Вижте пълните дневници" +modalTitle = "Известия" [offline] +configurationError = "Грешка при стартиране. Проверете конфигурацията си и рестартирайте." +message = "Няма връзка със сървъра." +restart = "Рестартиране" +restartNeeded = "Необходимо за прилагане на промените." +restarting = "Сървърът ще бъде отново на линия след малко." [passwordModal] +description = "Задайте парола за защита на настройките на конфигурацията. Използването на основния екран е възможно и без влизане в системата." +empty = "Паролата не трябва да бъде празна." +error = "Грешка: " +labelCurrent = "Текуща парола" +labelNew = "Нова парола" +labelRepeat = "Повторете паролата" +newPassword = "Създайте парола" +noMatch = "Паролите не съвпадат." +titleNew = "Задайте парола на администратора" +titleUpdate = "Обновяване на паролата на администратора" +updatePassword = "Обновяване на паролата" [session] +cancel = "Отказ" +co2 = "CO₂" +date = "Период" +delete = "Изтрий" +finished = "Завършено" +meter = "Километраж" +meterstart = "Начало на брояча" +meterstop = "Край на брояча" +odometer = "Пробег" +price = "Цена" +started = "Начално време" +title = "Сесия на зареждане" [sessions] +avgPower = "⌀ Мощност" +avgPrice = "⌀ Цена" +chargeDuration = "Продължителност" +co2 = "⌀ CO₂" +csvMonth = "Изтегляне на {month} CSV" +csvTotal = "Изтегляне на общ CSV" +date = "Начало" +downloadCsv = "Изтегляне като CSV" +energy = "Заредено" +loadpoint = "Ладеен пункт" +noData = "Няма сесии на зареждане този месец." +overview = "Общ преглед" +price = "Цена" +reallyDelete = "Наистина ли искате да изтриете тази сесия?" +showIndividualEntries = "Показване на отделни сесии" +solar = "Слънчева енергия" +title = "Сесии на зареждане" +total = "Общо" +vehicle = "Превозно средство" [sessions.chartTitle] +avgCo2ByGroup = "⌀ CO₂ {byGroup}" +avgPriceByGroup = "⌀ Цена {byGroup}" +byGroupLoadpoint = "По зарядна точка" +byGroupVehicle = "По превозно средство" +energy = "Заредена енергия" +energyGrouped = "Слънчева енергия срещу мрежова енергия" +energyGroupedByGroup = "Енергия {byGroup}" +energySubSolar = "{value} слънчева енергия" +energySubTotal = "{value} общо" +groupedCo2ByGroup = "Количество CO₂ {byGroup}" +groupedPriceByGroup = "Обща цена {byGroup}" +historyCo2 = "CO₂-емисии" +historyCo2Sub = "{value} общо" +historyPrice = "Разходи за зареждане" +historyPriceSub = "{value} общо" +solar = "Делът на слънчевата енергия в електроенергията през годината" +solarByGroup = "Слънчев дял {byGroup}" [sessions.csv] +chargedenergy = "Енергия (kWh)" +chargeduration = "Продължителност" +co2perkwh = "CO₂/kWh" +created = "Начално време" +finished = "Завършено" +identifier = "Идентификатор" +loadpoint = "Зарядна точка" +meterstart = "Начално показание на електромера (kWh)" +meterstop = "Крайно показание на електромера (kWh)" +odometer = "Пробег (км)" +price = "Цена" +priceperkwh = "Цена/kWh" +solarpercentage = "Слънчева енергия (%)" +vehicle = "Превозно средство" [sessions.filter] +allLoadpoints = "Всички зарядни точки" +allVehicles = "Всички превозни средства" +filter = "Филтър" [sessions.group] +co2 = "Емисии" +grid = "Мрежа" +price = "Цена" +self = "Слънчева енергия" [sessions.groupBy] +loadpoint = "Зарядна точка" +none = "Общо" +vehicle = "Превозно средство" [sessions.period] +month = "Месец" +total = "Общо" +year = "Година" [sessions.type] +co2 = "CO₂" +price = "Цена" +solar = "Слънчева енергия" [settings] +title = "Потребителски интерфейс" [settings.fullscreen] +enter = "Въведете в режим на цял екран" +exit = "Изход от режим на цял екран" +label = "Цял екран" [settings.hiddenFeatures] +label = "Експериментален" +value = "Показване на експериментални функции на потребителския интерфейс." [settings.language] +auto = "Автоматичен" +label = "Език" [settings.sponsorToken] +expires = "Вашият спонсорски токен изтича след {inXDays}. {getNewToken} и го актуализирайте тук." +getNew = "Вземете нов" +hint = "Забележка: В бъдеще ще автоматизираме това." [settings.telemetry] +label = "Телеметрия" [settings.theme] +auto = "Система" +dark = "Тъмен" +label = "Дизайн" +light = "Светъл" [settings.unit] +km = "km" +label = "Единици" +mi = "Мили" [smartCost] activeHours = "{charging} от {total}" +activeHoursLabel = "Активни часове" +applyToAll = "Приложи навсякъде?" +batteryDescription = "Зарежда домашната батерия с енергия от мрежата." +cheapTitle = "Евтино зареждане от мрежата" +cleanTitle = "Чисто зареждане от мрежата" +co2Label = "Емисия на CO₂" +co2Limit = "Граница на CO₂" +loadpointDescription = "Позволява временно бързо зареждане в соларен режим." +modalTitle = "Интелигентно зареждане от мрежата" +none = "Няма" +priceLabel = "Цена на енергията" +priceLimit = "Граница на цената" +saved = "Запазено." [startupError] +configFile = "Използван конфигурационен файл:" +configuration = "Конфигурация" +description = "Моля, проверете конфигурационния си файл. Ако съобщението за грешка не помогне, проверете {0}." +discussions = "Дискусии в GitHub" +fixAndRestart = "Моля, отстранете проблема и рестартирайте сървъра." +hint = "Забележка: Възможно е също така да имате дефектно устройство (инвертор, измервателен уред и т.н.). Проверете мрежовите си връзки." +lineError = "Грешка в {0}." +lineErrorLink = "Ред {0}" +restartButton = "Рестартиране" +title = "Грешка при стартиране" diff --git a/i18n/hr.toml b/i18n/hr.toml index d7406b833b..7c34332b6a 100644 --- a/i18n/hr.toml +++ b/i18n/hr.toml @@ -586,6 +586,7 @@ scale3p = "Uskoro se prebacuje na trofazno punjenje." targetChargeActive = "Aktivan je plan punjenja. Procijenjeni završetak za {duration}." targetChargePlanned = "Planirano punjenje počinje za {duration}." targetChargeWaitForVehicle = "Planirano punjenje je spremno. Čeka se na vozilo…" +unknown = "" vehicleLimit = "Granica vozila." vehicleLimitReached = "Dosegnuto ograničenje na vozilu." waitForVehicle = "Spremno. Čeka se na vozilo …" diff --git a/i18n/tr.toml b/i18n/tr.toml index bd6e2e8024..a02c510a41 100644 --- a/i18n/tr.toml +++ b/i18n/tr.toml @@ -402,7 +402,7 @@ titleMinSoc = "Asgari doldurma" titleTargetCharge = "Ayrılış" unsavedChanges = "Kaydedilmemiş değişiklikler var. Şimdi uygulansın mı?" update = "Uygula" -weekdays = "“Hafta günleri”" +weekdays = "Günler" [main.energyflow] battery = "Batarya" @@ -576,6 +576,7 @@ scale3p = "Birazdan 3 fazla doldurmaya yükseltilecek." targetChargeActive = "Doldurma planı yürürlükte. Tahmini bitiş süresi {duration} içerisinde." targetChargePlanned = "Doldurma planı {duration} içerisinde başlayacak." targetChargeWaitForVehicle = "Doldurma planı hazır. Araç bekleniyor…" +unknown = "" vehicleLimit = "Araç sınırı." vehicleLimitReached = "Araç sınırına ulaşıldı." vehicleTargetReached = "Araç sınırı {soc} ulaşıldı." From 933820a8101933cf4f01667b48047ddff6020a4c Mon Sep 17 00:00:00 2001 From: pdeliot Date: Fri, 3 Jan 2025 14:30:55 +0100 Subject: [PATCH 06/25] Enphase Envoy: add currents (#17193) --- templates/definition/meter/enphase.yaml | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/templates/definition/meter/enphase.yaml b/templates/definition/meter/enphase.yaml index 581e4c677d..7d22b15a1d 100644 --- a/templates/definition/meter/enphase.yaml +++ b/templates/definition/meter/enphase.yaml @@ -30,6 +30,34 @@ render: | insecure: true {{- end }} jq: .consumption[] | select(.measurementType == "net-consumption").wNow + currents: + - source: http + uri: http://{{ .host }}/production.json?details=1 + {{- if .token }} + auth: + type: bearer + password: {{ .token }} + insecure: true + {{- end }} + jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[0].rmsCurrent else 0 end + - source: http + uri: http://{{ .host }}/production.json?details=1 + {{- if .token }} + auth: + type: bearer + password: {{ .token }} + insecure: true + {{- end }} + jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[1].rmsCurrent else 0 end + - source: http + uri: http://{{ .host }}/production.json?details=1 + {{- if .token }} + auth: + type: bearer + password: {{ .token }} + insecure: true + {{- end }} + jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[2].rmsCurrent else 0 end {{- end }} {{- if eq .usage "pv" }} power: @@ -53,6 +81,34 @@ render: | {{- end }} jq: if (.production | length) > 1 and (.production[] | select(.measurementType == "production").activeCount >= 1) then .production[] | select(.measurementType == "production").whLifetime else .production[] | select(.type == "inverters").whLifetime end scale: 0.001 + currents: + - source: http + uri: http://{{ .host }}/production.json?details=1 + {{- if .token }} + auth: + type: bearer + password: {{ .token }} + insecure: true + {{- end }} + jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[0].rmsCurrent else 0 end + - source: http + uri: http://{{ .host }}/production.json?details=1 + {{- if .token }} + auth: + type: bearer + password: {{ .token }} + insecure: true + {{- end }} + jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[1].rmsCurrent else 0 end + - source: http + uri: http://{{ .host }}/production.json?details=1 + {{- if .token }} + auth: + type: bearer + password: {{ .token }} + insecure: true + {{- end }} + jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[2].rmsCurrent else 0 end {{- end }} {{- if eq .usage "battery" }} power: From 8e838a5202f65d64e8e5fc9b8487079a4726b3c5 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 3 Jan 2025 15:26:41 +0100 Subject: [PATCH 07/25] doc: use templates instead of types (#18027) --- .../Config/defaultYaml/tariffs.yaml | 3 +- cmd/demo.yaml | 3 +- evcc.dist.yaml | 77 ++----------------- 3 files changed, 9 insertions(+), 74 deletions(-) diff --git a/assets/js/components/Config/defaultYaml/tariffs.yaml b/assets/js/components/Config/defaultYaml/tariffs.yaml index f7e9e913ac..49b44c59d3 100644 --- a/assets/js/components/Config/defaultYaml/tariffs.yaml +++ b/assets/js/components/Config/defaultYaml/tariffs.yaml @@ -9,5 +9,6 @@ # price: 0.08 # EUR/kWh #co2: # carbon intensity forecast data -# type: grünstromindex +# type: template +# template: grünstromindex # zip: diff --git a/cmd/demo.yaml b/cmd/demo.yaml index acca352bee..c54a5b7ee0 100644 --- a/cmd/demo.yaml +++ b/cmd/demo.yaml @@ -263,5 +263,6 @@ tariffs: type: fixed price: 0.08 # EUR/kWh co2: - type: grünstromindex + type: template + template: grünstromindex zip: 12349 diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 08bb751296..ef51b0f23b 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -151,86 +151,19 @@ tariffs: price: 0.2 # EUR/kWh - days: Sat,Sun price: 0.15 # EUR/kWh - - # or variable tariffs - # type: tibber - # token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4" # access token - # homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c" # optional if multiple homes associated to account - - # type: awattar - # region: de # optional, choose at for Austria - # charges: # optional, additional charges per kWh - # tax: # optional, additional tax (0.1 for 10%) - - # type: octopusenergy - # tariff: AGILE-FLEX-22-11-25 # Tariff code - # region: A # optional - - # type: elering # Nordpool - # region: ee # or lt, lv, fi - # charges: # optional, additional charges per kWh - # tax: # optional, additional tax (0.1 for 10%) - - # type: energinet # Energinet using the price in DKK - # region: dk1 # or dk2 - # charges: # optional, additional charges per kWh - # tax: # optional, additional tax (0.1 for 10%) - - # type: entsoe # Entso-E european market data - # domain: BZN|DE-LU # https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas - # securitytoken: # api token - # charges: # optional, additional charges per kWh - # tax: # optional, additional tax (0.1 for 10%) - - # type: pun # PUN - Prezzo unico nazionale - Hourly Italian wholesale prices - # charges: 0 # optional, additional charges per kWh - # tax: 0 # optional, additional tax (0.1 for 10%) - - # type: amber - # token: # api token from https://app.amber.com.au/developers/ - # siteid: # site ID returned by the API - # channel: general - - # type: custom # price from a plugin source; see https://docs.evcc.io/docs/reference/plugins - # price: - # source: http - # uri: https://example.org/price.json - # jq: .price.current - - # type: template - # template: energy-charts-api # epex spot market prices - # bzn: DE-LU - # charges: 0.15 - # tax: 0.1 - # formula: math.Min((price + charges) * (1 + tax), 0.5) + # see: https://docs.evcc.io/en/docs/devices/tariffs feedin: # rate for feeding excess (pv) energy to the grid type: fixed price: 0.08 # EUR/kWh - - # type: octopusenergy - # tariff: AGILE-FLEX-22-11-25 # Tariff code - # region: A # optional - - # type: amber - # token: # api token from https://app.amber.com.au/developers/ - # siteid: # site ID returned by the API - # channel: feedIn + # see: https://docs.evcc.io/en/docs/devices/tariffs co2: # co2 tariff provides co2 intensity forecast and is for co2-optimized target charging if no variable grid tariff is specified - # type: grünstromindex # GrünStromIndex (Germany only) + # type: template + # template: grünstromindex # GrünStromIndex (Germany only) # zip: - - # type: electricitymaps # https://app.electricitymaps.com/map - # uri: - # token: # needs to be a token with forecast (not in the free tier) - # zone: DE - - # type: ngeso # National Grid Electricity System Operator data (Great Britain only) https://carbonintensity.org.uk/ - # provides national data if both region and postcode are omitted - Choose ONE only! - # region: 1 # optional, coarser than using a postcode - The region details are at https://carbon-intensity.github.io/api-definitions/#region-list - # postcode: SW1 # optional - Outward postcode i.e. RG41 or SW1 or TF8. Do not include full postcode, outward postcode only + # see: https://docs.evcc.io/en/docs/devices/tariffs#co-forecast # mqtt message broker mqtt: From a83ad42a03690d99ab82408c2ba27c5a2443f5f6 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 3 Jan 2025 16:50:28 +0100 Subject: [PATCH 08/25] Site: refactor measurements (#17943) --- core/keys/site.go | 4 +- core/site.go | 288 +++++++++++++++++----------------------------- 2 files changed, 109 insertions(+), 183 deletions(-) diff --git a/core/keys/site.go b/core/keys/site.go index 4ffbb14714..11784db602 100644 --- a/core/keys/site.go +++ b/core/keys/site.go @@ -3,7 +3,9 @@ package keys const ( Aux = "aux" AuxPower = "auxPower" + Circuits = "circuits" Currency = "currency" + Ext = "ext" GreenShareHome = "greenShareHome" GreenShareLoadpoints = "greenShareLoadpoints" GridConfigured = "gridConfigured" @@ -28,8 +30,6 @@ const ( TariffPriceHome = "tariffPriceHome" TariffPriceLoadpoints = "tariffPriceLoadpoints" Vehicles = "vehicles" - Circuits = "circuits" - Ext = "ext" // meters GridMeter = "gridMeter" diff --git a/core/site.go b/core/site.go index 1e1b2df0d2..a9cccbdec0 100644 --- a/core/site.go +++ b/core/site.go @@ -42,20 +42,14 @@ type updater interface { Update(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64) } -// meterMeasurement is used as slice element for publishing structured data -type meterMeasurement struct { - Power float64 `json:"power"` - Energy float64 `json:"energy,omitempty"` - ExcessDCPower float64 `json:"excessdcpower,omitempty"` -} - -// batteryMeasurement is used as slice element for publishing structured data -type batteryMeasurement struct { - Power float64 `json:"power"` - Energy float64 `json:"energy,omitempty"` - Soc float64 `json:"soc,omitempty"` - Capacity float64 `json:"capacity,omitempty"` - Controllable bool `json:"controllable"` +// measurement is used as slice element for publishing structured data +type measurement struct { + Power float64 `json:"power"` + Energy float64 `json:"energy,omitempty"` + ExcessDCPower float64 `json:"excessdcpower,omitempty"` + Capacity *float64 `json:"capacity,omitempty"` + Soc *float64 `json:"soc,omitempty"` + Controllable *bool `json:"controllable,omitempty"` } var _ site.API = (*Site)(nil) @@ -434,25 +428,17 @@ func (site *Site) publishDelta(key string, val interface{}) { site.publish(key, val) } -// updatePvMeters updates pv meters. All measurements are optional. -func (site *Site) updatePvMeters() { - if len(site.pvMeters) == 0 { - return - } - +func (site *Site) collectMeters(key string, meters []api.Meter) []measurement { var wg sync.WaitGroup - - mm := make([]meterMeasurement, len(site.pvMeters)) + mm := make([]measurement, len(meters)) fun := func(i int, meter api.Meter) { // power power, err := backoff.RetryWithData(meter.CurrentPower, bo()) if err == nil { - if power < -500 { - site.log.WARN.Printf("pv %d power: %.0fW is negative - check configuration if sign is correct", i+1, power) - } + site.log.DEBUG.Printf("%s %d power: %.0fW", key, i+1, power) } else { - site.log.ERROR.Printf("pv %d power: %v", i+1, err) + site.log.ERROR.Printf("%s %d power: %v", key, i+1, err) } // energy (production) @@ -460,171 +446,95 @@ func (site *Site) updatePvMeters() { if m, ok := meter.(api.MeterEnergy); err == nil && ok { energy, err = m.TotalEnergy() if err != nil { - site.log.ERROR.Printf("pv %d energy: %v", i+1, err) + site.log.ERROR.Printf("%s %d energy: %v", key, i+1, err) } } - var excessDC float64 - var excessStr string - if m, ok := meter.(api.MaxACPower); ok { - if dc := m.MaxACPower() - power; dc < 0 && power > 0 { - excessDC = -dc - excessStr = fmt.Sprintf(" (includes %.0fW excess DC)", -dc) - } - } - - if len(site.pvMeters) > 1 { - site.log.DEBUG.Printf("pv %d power: %.0fW"+excessStr, i+1, power) - } - - mm[i] = meterMeasurement{ - Power: power, - Energy: energy, - ExcessDCPower: excessDC, + mm[i] = measurement{ + Power: power, + Energy: energy, } wg.Done() } - wg.Add(len(site.pvMeters)) - for i, meter := range site.pvMeters { + wg.Add(len(meters)) + for i, meter := range meters { go fun(i, meter) } wg.Wait() - site.pvPower = lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 { - return acc + max(0, m.Power) - }, 0) - site.excessDCPower = lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 { - return acc - math.Abs(m.ExcessDCPower) - }, 0) - totalEnergy := lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 { - return acc + m.Energy - }, 0) - - var excessStr string - if site.excessDCPower < 0 { - excessStr = fmt.Sprintf(" (includes %.0fW excess DC)", -site.excessDCPower) - } - - site.log.DEBUG.Printf("pv power: %.0fW"+excessStr, site.pvPower) - site.publish(keys.PvPower, site.pvPower) - site.publish(keys.PvEnergy, totalEnergy) - site.publish(keys.Pv, mm) + return mm } -// updateAuxMeters updates aux meters -func (site *Site) updateAuxMeters() { - if len(site.auxMeters) == 0 { +// updatePvMeters updates pv meters. All measurements are optional. +func (site *Site) updatePvMeters() { + if len(site.pvMeters) == 0 { return } - var wg sync.WaitGroup + mm := site.collectMeters("pv", site.pvMeters) - mm := make([]meterMeasurement, len(site.auxMeters)) + for i, meter := range site.pvMeters { + power := mm[i].Power - fun := func(i int, meter api.Meter) { - if power, err := meter.CurrentPower(); err == nil { - mm[i].Power = power - site.log.DEBUG.Printf("aux power %d: %.0fW", i+1, power) - } else { - site.log.ERROR.Printf("aux meter %d: %v", i+1, err) + if power < -500 { + site.log.WARN.Printf("pv %d power: %.0fW is negative - check configuration if sign is correct", i+1, power) } - wg.Done() - } - - wg.Add(len(site.auxMeters)) - for i, meter := range site.auxMeters { - go fun(i, meter) + if m, ok := meter.(api.MaxACPower); ok { + if dc := m.MaxACPower() - power; dc < 0 && power > 0 { + mm[i].ExcessDCPower = -dc + site.log.DEBUG.Printf("pv %d excess DC: %.0fW", i+1, -dc) + } + } } - wg.Wait() - site.auxPower = lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 { - return acc + m.Power + site.pvPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { + return acc + max(0, m.Power) + }, 0) + site.excessDCPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { + return acc - math.Abs(m.ExcessDCPower) + }, 0) + totalEnergy := lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { + return acc + m.Energy }, 0) - site.log.DEBUG.Printf("aux power: %.0fW", site.auxPower) - site.publish(keys.AuxPower, site.auxPower) - site.publish(keys.Aux, mm) -} - -// updateExtMeters updates ext meters -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) - } + if len(site.pvMeters) > 1 { + var excessStr string + if site.excessDCPower < 0 { + excessStr = fmt.Sprintf(" (includes %.0fW excess DC)", -site.excessDCPower) } - mm[i] = meterMeasurement{ - Power: power, - Energy: energy, - } + site.log.DEBUG.Printf("pv power: %.0fW"+excessStr, site.pvPower) } - // Publishing will be done in separate PR + site.publish(keys.PvPower, site.pvPower) + site.publish(keys.PvEnergy, totalEnergy) + site.publish(keys.Pv, mm) } // updateBatteryMeters updates battery meters -func (site *Site) updateBatteryMeters() error { +func (site *Site) updateBatteryMeters() { if len(site.batteryMeters) == 0 { - return nil + return } - var eg errgroup.Group - - mm := make([]batteryMeasurement, len(site.batteryMeters)) - - fun := func(i int, meter api.Meter) error { - power, err := backoff.RetryWithData(meter.CurrentPower, bo()) - if err != nil { - // power is required- return on error - return fmt.Errorf("battery %d power: %v", i+1, err) - } - - if len(site.batteryMeters) > 1 { - site.log.DEBUG.Printf("battery %d power: %.0fW", i+1, power) - } - - // battery energy (discharge) - var energy float64 - if m, ok := meter.(api.MeterEnergy); ok { - energy, err = m.TotalEnergy() - if err != nil { - site.log.ERROR.Printf("battery %d energy: %v", i+1, err) - } - } + mm := site.collectMeters("battery", site.batteryMeters) + for i, meter := range site.batteryMeters { // battery soc and capacity var batSoc, capacity float64 - if meter, ok := meter.(api.Battery); ok { - batSoc, err = soc.Guard(meter.Soc()) + var err error + if m, ok := meter.(api.Battery); ok { + batSoc, err = soc.Guard(m.Soc()) if err == nil { - if m, ok := meter.(api.BatteryCapacity); ok { + if m, ok := m.(api.BatteryCapacity); ok { capacity = m.Capacity() } - if len(site.batteryMeters) > 1 { - site.log.DEBUG.Printf("battery %d soc: %.0f%%", i+1, batSoc) - } + site.log.DEBUG.Printf("battery %d soc: %.0f%%", i+1, batSoc) } else { site.log.ERROR.Printf("battery %d soc: %v", i+1, err) } @@ -632,35 +542,21 @@ func (site *Site) updateBatteryMeters() error { _, controllable := meter.(api.BatteryController) - mm[i] = batteryMeasurement{ - Power: power, - Energy: energy, - Soc: batSoc, - Capacity: capacity, - Controllable: controllable, - } - - return nil + mm[i].Soc = lo.ToPtr(batSoc) + mm[i].Capacity = lo.ToPtr(capacity) + mm[i].Controllable = lo.ToPtr(controllable) } - for i, meter := range site.batteryMeters { - eg.Go(func() error { return fun(i, meter) }) - } - - if err := eg.Wait(); err != nil { - return err - } - - site.batterySoc = lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 { + site.batterySoc = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { // weigh soc by capacity - weighedSoc := m.Soc - if m.Capacity > 0 { - weighedSoc *= m.Capacity + weighedSoc := *m.Soc + if *m.Capacity > 0 { + weighedSoc *= *m.Capacity } return acc + weighedSoc }, 0) - totalCapacity := lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 { - return acc + m.Capacity + totalCapacity := lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { + return acc + *m.Capacity }, 0) // convert weighed socs to total soc @@ -669,23 +565,53 @@ func (site *Site) updateBatteryMeters() error { } site.batterySoc /= totalCapacity - site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.batterySoc)) - site.publish(keys.BatteryCapacity, totalCapacity) - site.publish(keys.BatterySoc, site.batterySoc) - - site.batteryPower = lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 { + site.batteryPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { return acc + m.Power }, 0) - totalEnergy := lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 { + totalEnergy := lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { return acc + m.Energy }, 0) - site.log.DEBUG.Printf("battery power: %.0fW", site.batteryPower) + if len(site.batteryMeters) > 1 { + site.log.DEBUG.Printf("battery power: %.0fW", site.batteryPower) + site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.batterySoc)) + } + + site.publish(keys.BatteryCapacity, totalCapacity) + site.publish(keys.BatterySoc, site.batterySoc) + site.publish(keys.BatteryPower, site.batteryPower) site.publish(keys.BatteryEnergy, totalEnergy) site.publish(keys.Battery, mm) +} - return nil +// updateAuxMeters updates aux meters +func (site *Site) updateAuxMeters() { + if len(site.auxMeters) == 0 { + return + } + + mm := site.collectMeters("aux", site.auxMeters) + site.auxPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 { + return acc + m.Power + }, 0) + + if len(site.auxMeters) > 1 { + site.log.DEBUG.Printf("aux power: %.0fW", site.auxPower) + } + + site.publish(keys.AuxPower, site.auxPower) + site.publish(keys.Aux, mm) +} + +// updateExtMeters updates ext meters +func (site *Site) updateExtMeters() { + if len(site.extMeters) == 0 { + return + } + + mm := site.collectMeters("ext", site.extMeters) + site.publish(keys.Ext, mm) } // updateGridMeter updates grid meter @@ -742,10 +668,10 @@ func (site *Site) updateMeters() error { var eg errgroup.Group eg.Go(func() error { site.updatePvMeters(); return nil }) + eg.Go(func() error { site.updateBatteryMeters(); return nil }) eg.Go(func() error { site.updateAuxMeters(); return nil }) eg.Go(func() error { site.updateExtMeters(); return nil }) - eg.Go(site.updateBatteryMeters) eg.Go(site.updateGridMeter) return eg.Wait() @@ -769,7 +695,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b // ensure safe default for residual power residualPower := site.GetResidualPower() if len(site.batteryMeters) > 0 && site.batterySoc < site.prioritySoc && residualPower <= 0 { - residualPower = 100 // W + residualPower = 100 // Wsite.publish(keys.PvPower, } // allow using grid and charge as estimate for pv power From 07070a47d1ca3442ff18794b2332408e71ef3ca2 Mon Sep 17 00:00:00 2001 From: andig Date: Fri, 3 Jan 2025 18:46:47 +0100 Subject: [PATCH 09/25] chore: refactor --- charger/fritzdect.go | 5 ++++- charger/homematic.go | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/charger/fritzdect.go b/charger/fritzdect.go index 81a0fc9fa1..81cd659cca 100644 --- a/charger/fritzdect.go +++ b/charger/fritzdect.go @@ -44,6 +44,9 @@ func NewFritzDECTFromConfig(other map[string]interface{}) (api.Charger, error) { // NewFritzDECT creates a new connection with standbypower for charger func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64) (*FritzDECT, error) { conn, err := fritzdect.NewConnection(uri, ain, user, password) + if err != nil { + return nil, err + } c := &FritzDECT{ conn: conn, @@ -51,7 +54,7 @@ func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower flo c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower) - return c, err + return c, nil } // Status implements the api.Charger interface diff --git a/charger/homematic.go b/charger/homematic.go index 30c547f339..ec2a7382cd 100644 --- a/charger/homematic.go +++ b/charger/homematic.go @@ -44,6 +44,9 @@ func NewCCUFromConfig(other map[string]interface{}) (api.Charger, error) { // NewCCU creates a new connection with standbypower for charger func NewCCU(embed embed, uri, deviceid, meterid, switchid, user, password string, standbypower float64, cache time.Duration) (*CCU, error) { conn, err := homematic.NewConnection(uri, deviceid, meterid, switchid, user, password, cache) + if err != nil { + return nil, err + } c := &CCU{ conn: conn, @@ -51,7 +54,7 @@ func NewCCU(embed embed, uri, deviceid, meterid, switchid, user, password string c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower) - return c, err + return c, nil } // Enabled implements the api.Charger interface From 41da4199be2f97feb792b2e4061d33504ecef692 Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 Jan 2025 15:37:59 +0100 Subject: [PATCH 10/25] Add myPV AC Thor (#18034) --- charger/{mypv-elwa2.go => mypv.go} | 81 +++++++++++++---------- templates/definition/charger/ac-thor.yaml | 14 ++++ 2 files changed, 59 insertions(+), 36 deletions(-) rename charger/{mypv-elwa2.go => mypv.go} (64%) create mode 100644 templates/definition/charger/ac-thor.yaml diff --git a/charger/mypv-elwa2.go b/charger/mypv.go similarity index 64% rename from charger/mypv-elwa2.go rename to charger/mypv.go index 151e2f6af8..c6a5d27589 100644 --- a/charger/mypv-elwa2.go +++ b/charger/mypv.go @@ -29,11 +29,12 @@ import ( "github.com/evcc-io/evcc/util/sponsor" ) -// MyPvElwa2 charger implementation -type MyPvElwa2 struct { - log *util.Logger - conn *modbus.Connection - power uint32 +// MyPv charger implementation +type MyPv struct { + log *util.Logger + conn *modbus.Connection + power uint32 + regPower uint16 } const ( @@ -42,16 +43,23 @@ const ( elwaRegTempLimit = 1002 elwaRegStatus = 1003 elwaRegPower = 1074 + thorRegPower = 1060 ) func init() { - registry.AddCtx("ac-elwa-2", NewMyPvElwa2FromConfig) -} + // https://github.com/evcc-io/evcc/discussions/12761 + registry.AddCtx("ac-elwa-2", func(ctx context.Context, other map[string]interface{}) (api.Charger, error) { + return newMyPvFromConfig(ctx, "ac-elwa-2", other, elwaRegPower) + }) -// https://github.com/evcc-io/evcc/discussions/12761 + // https: // github.com/evcc-io/evcc/issues/18020 + registry.AddCtx("ac-thor", func(ctx context.Context, other map[string]interface{}) (api.Charger, error) { + return newMyPvFromConfig(ctx, "ac-thor", other, thorRegPower) + }) +} -// NewMyPvElwa2FromConfig creates a MyPvElwa2 charger from generic config -func NewMyPvElwa2FromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) { +// newMyPvFromConfig creates a MyPv charger from generic config +func newMyPvFromConfig(ctx context.Context, name string, other map[string]interface{}, regPower uint16) (api.Charger, error) { cc := modbus.TcpSettings{ ID: 1, } @@ -60,11 +68,11 @@ func NewMyPvElwa2FromConfig(ctx context.Context, other map[string]interface{}) ( return nil, err } - return NewMyPvElwa2(ctx, cc.URI, cc.ID) + return NewMyPv(ctx, name, cc.URI, cc.ID, regPower) } -// NewMyPvElwa2 creates myPV AC Elwa 2 charger -func NewMyPvElwa2(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) { +// NewMyPv creates myPV AC Elwa 2 or Thor charger +func NewMyPv(ctx context.Context, name, uri string, slaveID uint8, regPower uint16) (api.Charger, error) { conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID) if err != nil { return nil, err @@ -74,12 +82,13 @@ func NewMyPvElwa2(ctx context.Context, uri string, slaveID uint8) (api.Charger, return nil, api.ErrSponsorRequired } - log := util.NewLogger("ac-elwa-2") + log := util.NewLogger(name) conn.Logger(log.TRACE) - wb := &MyPvElwa2{ - log: log, - conn: conn, + wb := &MyPv{ + log: log, + conn: conn, + regPower: regPower, } go wb.heartbeat(ctx, 30*time.Second) @@ -87,21 +96,21 @@ func NewMyPvElwa2(ctx context.Context, uri string, slaveID uint8) (api.Charger, return wb, nil } -var _ api.IconDescriber = (*MyPvElwa2)(nil) +var _ api.IconDescriber = (*MyPv)(nil) // Icon implements the api.IconDescriber interface -func (v *MyPvElwa2) Icon() string { +func (v *MyPv) Icon() string { return "waterheater" } -var _ api.FeatureDescriber = (*MyPvElwa2)(nil) +var _ api.FeatureDescriber = (*MyPv)(nil) // Features implements the api.FeatureDescriber interface -func (wb *MyPvElwa2) Features() []api.Feature { +func (wb *MyPv) Features() []api.Feature { return []api.Feature{api.IntegratedDevice, api.Heating} } -func (wb *MyPvElwa2) heartbeat(ctx context.Context, timeout time.Duration) { +func (wb *MyPv) heartbeat(ctx context.Context, timeout time.Duration) { for tick := time.Tick(timeout); ; { select { case <-tick: @@ -122,7 +131,7 @@ func (wb *MyPvElwa2) heartbeat(ctx context.Context, timeout time.Duration) { } // Status implements the api.Charger interface -func (wb *MyPvElwa2) Status() (api.ChargeStatus, error) { +func (wb *MyPv) Status() (api.ChargeStatus, error) { res := api.StatusA b, err := wb.conn.ReadHoldingRegisters(elwaRegStatus, 1) if err != nil { @@ -138,7 +147,7 @@ func (wb *MyPvElwa2) Status() (api.ChargeStatus, error) { } // Enabled implements the api.Charger interface -func (wb *MyPvElwa2) Enabled() (bool, error) { +func (wb *MyPv) Enabled() (bool, error) { b, err := wb.conn.ReadHoldingRegisters(elwaRegSetPower, 1) if err != nil { return false, err @@ -147,7 +156,7 @@ func (wb *MyPvElwa2) Enabled() (bool, error) { return binary.BigEndian.Uint16(b) > 0, nil } -func (wb *MyPvElwa2) setPower(power uint16) error { +func (wb *MyPv) setPower(power uint16) error { b := make([]byte, 2) binary.BigEndian.PutUint16(b, power) @@ -156,7 +165,7 @@ func (wb *MyPvElwa2) setPower(power uint16) error { } // Enable implements the api.Charger interface -func (wb *MyPvElwa2) Enable(enable bool) error { +func (wb *MyPv) Enable(enable bool) error { var power uint16 if enable { power = uint16(atomic.LoadUint32(&wb.power)) @@ -166,14 +175,14 @@ func (wb *MyPvElwa2) Enable(enable bool) error { } // MaxCurrent implements the api.Charger interface -func (wb *MyPvElwa2) MaxCurrent(current int64) error { +func (wb *MyPv) MaxCurrent(current int64) error { return wb.MaxCurrentMillis(float64(current)) } -var _ api.ChargerEx = (*MyPvElwa2)(nil) +var _ api.ChargerEx = (*MyPv)(nil) // MaxCurrentMillis implements the api.ChargerEx interface -func (wb *MyPvElwa2) MaxCurrentMillis(current float64) error { +func (wb *MyPv) MaxCurrentMillis(current float64) error { power := uint16(230 * current) err := wb.setPower(power) @@ -184,11 +193,11 @@ func (wb *MyPvElwa2) MaxCurrentMillis(current float64) error { return err } -var _ api.Meter = (*MyPvElwa2)(nil) +var _ api.Meter = (*MyPv)(nil) // CurrentPower implements the api.Meter interface -func (wb *MyPvElwa2) CurrentPower() (float64, error) { - b, err := wb.conn.ReadHoldingRegisters(elwaRegPower, 1) +func (wb *MyPv) CurrentPower() (float64, error) { + b, err := wb.conn.ReadHoldingRegisters(wb.regPower, 1) if err != nil { return 0, err } @@ -196,10 +205,10 @@ func (wb *MyPvElwa2) CurrentPower() (float64, error) { return float64(binary.BigEndian.Uint16(b)), nil } -var _ api.Battery = (*MyPvElwa2)(nil) +var _ api.Battery = (*MyPv)(nil) // CurrentPower implements the api.Meter interface -func (wb *MyPvElwa2) Soc() (float64, error) { +func (wb *MyPv) Soc() (float64, error) { b, err := wb.conn.ReadHoldingRegisters(elwaRegTemp, 1) if err != nil { return 0, err @@ -208,10 +217,10 @@ func (wb *MyPvElwa2) Soc() (float64, error) { return float64(binary.BigEndian.Uint16(b)) / 10, nil } -var _ api.SocLimiter = (*MyPvElwa2)(nil) +var _ api.SocLimiter = (*MyPv)(nil) // GetLimitSoc implements the api.SocLimiter interface -func (wb *MyPvElwa2) GetLimitSoc() (int64, error) { +func (wb *MyPv) GetLimitSoc() (int64, error) { b, err := wb.conn.ReadHoldingRegisters(elwaRegTempLimit, 1) if err != nil { return 0, err diff --git a/templates/definition/charger/ac-thor.yaml b/templates/definition/charger/ac-thor.yaml new file mode 100644 index 0000000000..534b287cd3 --- /dev/null +++ b/templates/definition/charger/ac-thor.yaml @@ -0,0 +1,14 @@ +template: ac-thor +products: + - brand: my-PV + description: + generic: AC THOR, AC THOR 9s +group: heating +requirements: + evcc: ["sponsorship"] +params: + - name: modbus + choice: ["tcpip"] +render: | + type: ac-thor + {{- include "modbus" . }} From eaff6de83c20c81d09b36d1ddfbbd14eb1eeddaf Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 4 Jan 2025 16:25:41 +0100 Subject: [PATCH 11/25] Smart EQ: use Mercedes api (#18044) --- templates/definition/vehicle/smart.yaml | 34 +++++++++++-- vehicle/mercedes.go | 35 ++++++++----- vehicle/smart.go | 65 ------------------------- 3 files changed, 55 insertions(+), 79 deletions(-) delete mode 100644 vehicle/smart.go diff --git a/templates/definition/vehicle/smart.yaml b/templates/definition/vehicle/smart.yaml index fb8d12e21d..dd6b6613cb 100644 --- a/templates/definition/vehicle/smart.yaml +++ b/templates/definition/vehicle/smart.yaml @@ -3,8 +3,36 @@ products: - brand: Smart description: generic: EQ +requirements: + description: + de: | + Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden. + en: | + Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`. params: - - preset: vehicle-base + - preset: vehicle-common + - name: user + required: true + - name: region + required: true + choice: [EMEA, APAC, NORAM] + default: EMEA + - name: accessToken + required: true + mask: true + - name: refreshToken + required: true + mask: true + - name: vin + example: V... + - name: cache + default: 15m render: | - type: smart - {{ include "vehicle-base" . }} + type: smart-eq + vin: {{ .vin }} + user: {{ .user }} + region: {{ .region }} + tokens: + access: {{ .accessToken }} + refresh: {{ .refreshToken }} + {{ include "vehicle-common" . }} diff --git a/vehicle/mercedes.go b/vehicle/mercedes.go index cfb461aef6..9eeab3f7f0 100644 --- a/vehicle/mercedes.go +++ b/vehicle/mercedes.go @@ -1,6 +1,7 @@ package vehicle import ( + "errors" "time" "github.com/evcc-io/evcc/api" @@ -15,11 +16,16 @@ type Mercedes struct { } func init() { - registry.Add("mercedes", NewMercedesFromConfig) + registry.Add("mercedes", func(other map[string]interface{}) (api.Vehicle, error) { + return newMercedesFromConfig("mercedes", other) + }) + registry.Add("smart-eq", func(other map[string]interface{}) (api.Vehicle, error) { + return newMercedesFromConfig("smart-eq", other) + }) } -// NewMercedesFromConfig creates a new vehicle -func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) { +// newMercedesFromConfig creates a new vehicle +func newMercedesFromConfig(brand string, other map[string]interface{}) (api.Vehicle, error) { cc := struct { embed `mapstructure:",squash"` Tokens Tokens @@ -45,21 +51,28 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) { cc.User = cc.Account_ } - log := util.NewLogger("mercedes").Redact(cc.Tokens.Access, cc.Tokens.Refresh) + log := util.NewLogger(brand).Redact(cc.Tokens.Access, cc.Tokens.Refresh) identity, err := mercedes.NewIdentity(log, token, cc.User, cc.Region) if err != nil { return nil, err } - v := &Mercedes{ - embed: &cc.embed, - } - api := mercedes.NewAPI(log, identity) - cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) - if err == nil { - v.Provider = mercedes.NewProvider(api, cc.VIN, cc.Cache) + if brand == "smart-eq" { + if cc.VIN == "" { + return nil, errors.New("missing VIN") + } + } else { + cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) + if err != nil { + return nil, err + } + } + + v := &Mercedes{ + embed: &cc.embed, + Provider: mercedes.NewProvider(api, cc.VIN, cc.Cache), } return v, err diff --git a/vehicle/smart.go b/vehicle/smart.go deleted file mode 100644 index 15eecb99a9..0000000000 --- a/vehicle/smart.go +++ /dev/null @@ -1,65 +0,0 @@ -package vehicle - -import ( - "fmt" - "time" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/vehicle/mb" - "github.com/evcc-io/evcc/vehicle/smart" -) - -// Smart is an api.Vehicle implementation for Smart cars -type Smart struct { - *embed - *smart.Provider -} - -func init() { - registry.Add("smart", NewSmartFromConfig) -} - -// NewSmartFromConfig creates a new vehicle -func NewSmartFromConfig(other map[string]interface{}) (api.Vehicle, error) { - cc := struct { - embed `mapstructure:",squash"` - User, Password string - VIN string - Expiry time.Duration - Cache time.Duration - }{ - Expiry: expiry, - Cache: interval, - } - - if err := util.DecodeOther(other, &cc); err != nil { - return nil, err - } - - if cc.User == "" || cc.Password == "" { - return nil, api.ErrMissingCredentials - } - - log := util.NewLogger("smart").Redact(cc.User, cc.Password, cc.VIN) - - v := &Smart{ - embed: &cc.embed, - } - - identity := mb.NewIdentity(log, smart.OAuth2Config) - err := identity.Login(cc.User, cc.Password) - if err != nil { - return v, fmt.Errorf("login failed: %w", err) - } - - api := smart.NewAPI(log, identity) - - cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles) - - if err == nil { - v.Provider = smart.NewProvider(log, api, cc.VIN, cc.Expiry, cc.Cache) - } - - return v, err -} From b728e23caf6b75caa909ca75a1adffd1cad3f673 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Sat, 4 Jan 2025 19:16:24 +0100 Subject: [PATCH 12/25] Battery grid charge: show configured limit (#18032) --- .../js/components/Energyflow/Energyflow.vue | 30 ++++++++++++------- i18n/bg.toml | 2 -- i18n/da.toml | 2 -- i18n/de.toml | 4 +-- i18n/el.toml | 2 -- i18n/en.toml | 4 +-- i18n/es.toml | 2 -- i18n/fi.toml | 2 -- i18n/fr.toml | 2 -- i18n/hr.toml | 2 -- i18n/hu.toml | 2 -- i18n/it.toml | 2 -- i18n/lb.toml | 2 -- i18n/lt.toml | 2 -- i18n/nl.toml | 2 -- i18n/pt.toml | 2 -- i18n/sl.toml | 2 -- i18n/sv.toml | 2 -- i18n/ta.toml | 2 -- i18n/tr.toml | 2 -- tests/battery-settings.spec.js | 6 +++- 21 files changed, 29 insertions(+), 49 deletions(-) diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue index 30f86aa68d..7601326577 100644 --- a/assets/js/components/Energyflow/Energyflow.vue +++ b/assets/js/components/Energyflow/Energyflow.vue @@ -103,7 +103,7 @@ data-testid="energyflow-entry-batterydischarge" @details-clicked="openBatterySettingsModal" > -