From 6ced552909b2331a2f92765e8e6249a9f8089aee Mon Sep 17 00:00:00 2001 From: nickles-lee Date: Wed, 3 Jun 2026 12:36:44 +0200 Subject: [PATCH 1/5] Feat: Add battery phase assignment support --- .DS_Store | Bin 6148 -> 0 bytes docs/06-advanced-features.md | 5 + home assistant/dashboard.yaml | 111 ++++++++++++--- .../packages/house_battery_control.yaml | 133 ++++++++++++++++++ node-red/01 start-flow.json | 94 ++++++++++++- node-red/all-flows-in-one-file.json | 94 ++++++++++++- 6 files changed, 405 insertions(+), 32 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 689fa65a68e6d679245cb828e19b9c04bd439204..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}(1u5S~p!*$B1Dp;R1K;u_RIB}&zcmBI^PR1SbryN-&5@uJvi4pAf@;e~jV zK2N{d-Bci2j)hh;*6g=GGwa=NtsM^$so^x+Ch8H9hBCIgD1IY6&iX)Fp{D~>ZVp2U zRd{l!$iKhDPU>kjOeQDXI-9_Kbw|Cy}_05O9cZ)?=uW!EncsRN!E=zmuX;%0GoxBXVgd0>I z-CmKG+<`S_Az8WnsI<<1ZIfTquSuhvE&W6Z9Ld0Hc77NAy7CN5Ih_g)s0} G8Tbj)V_%j4 diff --git a/docs/06-advanced-features.md b/docs/06-advanced-features.md index 0e17303..caa3988 100644 --- a/docs/06-advanced-features.md +++ b/docs/06-advanced-features.md @@ -64,6 +64,10 @@ nav_order: 6 ## Multi-Battery Management - **More than 6 batteries:** Override or change `input_number.house_battery_count` and you are good to go. - The dashboard supports 6 batteries out of the box. For 7 or more, duplicate and edit these cards or create your own dashboard. +- **Manual phase assignment:** Assign each configured battery to `L1`, `L2`, `L3`, or `Unassigned` from the dashboard. + - The overview shows the assigned phase on each battery header and shows live battery power per phase in kW. + - The Node-RED battery object exposes this as `battery.phase`, so custom strategies can use the mapping. + - Built-in strategies still use aggregate control by default; per-phase control/peak shaving is not enforced yet. - **3-Phase self-consumption:** if you require 0 W grid consumption on a per phase basis, the setup changes slightly. Note: most homes get billed for the net total of all phases. If that is the case for you as well, ignore these instructions. @@ -94,6 +98,7 @@ Controls grid import/export thresholds for `peak shaving` functionality. - **Import limit:** Maximum power to draw from the grid (example: 16A × 230V = 3680W for CAPTAR contracts) - **Export limit:** Maximum power to feed back to the grid (example: 3000W if grid connection has export limits) +- **Max phase power:** Shared per-phase safety threshold for future/custom phase-aware strategies. This value is exposed to Node-RED as `msg.grid_power_limit_phase`, but built-in strategies do not enforce it yet. - **Configuration:** Adjust from the "Settings" tab in the Home Assistant dashboard ### Charge / Sell Power Mode diff --git a/home assistant/dashboard.yaml b/home assistant/dashboard.yaml index d661ddf..8266b53 100644 --- a/home assistant/dashboard.yaml +++ b/home assistant/dashboard.yaml @@ -410,9 +410,22 @@ views: columns: full rows: 1 - type: heading - heading: Marstek M1 + heading: Phase power heading_style: subtitle - icon: '' + icon: mdi:transmission-tower + - type: glance + entities: + - entity: sensor.house_battery_l1_power + name: L1 + - entity: sensor.house_battery_l2_power + name: L2 + - entity: sensor.house_battery_l3_power + name: L3 + columns: 3 + - type: markdown + content: >- + ### Marstek M1{% set phase = states('input_select.marstek_m1_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} + text_only: true visibility: - condition: state entity: sensor.marstek_m1_device_name @@ -423,6 +436,8 @@ views: name: M1 SoC - entity: sensor.marstek_m1_ac_power name: M1 Power + - entity: input_select.marstek_m1_phase + name: Phase - entity: number.marstek_m1_max_charge_power name: Max Charge - entity: number.marstek_m1_max_discharge_power @@ -431,10 +446,10 @@ views: - condition: state entity: sensor.marstek_m1_device_name state_not: unknown - - type: heading - heading: Marstek M2 - heading_style: subtitle - icon: '' + - type: markdown + content: >- + ### Marstek M2{% set phase = states('input_select.marstek_m2_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} + text_only: true visibility: - condition: and conditions: @@ -450,6 +465,8 @@ views: name: M2 SoC - entity: sensor.marstek_m2_ac_power name: M2 Power + - entity: input_select.marstek_m2_phase + name: Phase - entity: number.marstek_m2_max_charge_power name: Max Charge - entity: number.marstek_m2_max_discharge_power @@ -463,10 +480,10 @@ views: - condition: numeric_state entity: input_number.house_battery_count above: 1 - - type: heading - heading: Marstek M3 - heading_style: subtitle - icon: '' + - type: markdown + content: >- + ### Marstek M3{% set phase = states('input_select.marstek_m3_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} + text_only: true visibility: - condition: and conditions: @@ -482,6 +499,8 @@ views: name: M3 SoC - entity: sensor.marstek_m3_ac_power name: M3 Power + - entity: input_select.marstek_m3_phase + name: Phase - entity: number.marstek_m3_max_charge_power name: Max Charge - entity: number.marstek_m3_max_discharge_power @@ -495,10 +514,10 @@ views: - condition: numeric_state entity: input_number.house_battery_count above: 2 - - type: heading - heading: Marstek M4 - heading_style: subtitle - icon: '' + - type: markdown + content: >- + ### Marstek M4{% set phase = states('input_select.marstek_m4_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} + text_only: true visibility: - condition: and conditions: @@ -514,6 +533,8 @@ views: name: M4 SoC - entity: sensor.marstek_m4_ac_power name: M4 Power + - entity: input_select.marstek_m4_phase + name: Phase - entity: number.marstek_m4_max_charge_power name: Max Charge - entity: number.marstek_m4_max_discharge_power @@ -527,10 +548,10 @@ views: - condition: numeric_state entity: input_number.house_battery_count above: 3 - - type: heading - heading: Marstek M5 - heading_style: subtitle - icon: '' + - type: markdown + content: >- + ### Marstek M5{% set phase = states('input_select.marstek_m5_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} + text_only: true visibility: - condition: and conditions: @@ -546,6 +567,8 @@ views: name: M5 SoC - entity: sensor.marstek_m5_ac_power name: M5 Power + - entity: input_select.marstek_m5_phase + name: Phase - entity: number.marstek_m5_max_charge_power name: Max Charge - entity: number.marstek_m5_max_discharge_power @@ -559,10 +582,10 @@ views: - condition: numeric_state entity: input_number.house_battery_count above: 4 - - type: heading - heading: Marstek M6 - heading_style: subtitle - icon: '' + - type: markdown + content: >- + ### Marstek M6{% set phase = states('input_select.marstek_m6_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} + text_only: true visibility: - condition: and conditions: @@ -578,6 +601,8 @@ views: name: M6 SoC - entity: sensor.marstek_m6_ac_power name: M6 Power + - entity: input_select.marstek_m6_phase + name: Phase - entity: number.marstek_m6_max_charge_power name: Max Charge - entity: number.marstek_m6_max_discharge_power @@ -1848,6 +1873,24 @@ views: text_only: true grid_options: columns: full + - type: grid + cards: + - type: heading + icon: mdi:fuse + heading: Per-phase safety limit + heading_style: subtitle + - type: entities + entities: + - entity: input_number.house_battery_control_max_phase_power + name: Max phase power + - type: markdown + content: >- + Available for custom and future phase-aware + strategies. Built-in strategies do not enforce this limit + yet. + text_only: true + grid_options: + columns: full - type: grid cards: - type: heading @@ -1890,6 +1933,10 @@ views: icon: mdi:power-plug-battery heading: Marstek M1 heading_style: title + - type: entities + entities: + - entity: input_select.marstek_m1_phase + name: Phase - type: vertical-stack cards: - type: markdown @@ -1959,6 +2006,10 @@ views: icon: mdi:power-plug-battery heading: Marstek M2 heading_style: title + - type: entities + entities: + - entity: input_select.marstek_m2_phase + name: Phase - type: vertical-stack cards: - type: markdown @@ -2024,6 +2075,10 @@ views: icon: mdi:power-plug-battery heading: Marstek M3 heading_style: title + - type: entities + entities: + - entity: input_select.marstek_m3_phase + name: Phase - type: vertical-stack cards: - type: markdown @@ -2089,6 +2144,10 @@ views: icon: mdi:power-plug-battery heading: Marstek M4 heading_style: title + - type: entities + entities: + - entity: input_select.marstek_m4_phase + name: Phase - type: vertical-stack cards: - type: markdown @@ -2154,6 +2213,10 @@ views: icon: mdi:power-plug-battery heading: Marstek M5 heading_style: title + - type: entities + entities: + - entity: input_select.marstek_m5_phase + name: Phase - type: vertical-stack cards: - type: markdown @@ -2219,6 +2282,10 @@ views: icon: mdi:power-plug-battery heading: Marstek M6 heading_style: title + - type: entities + entities: + - entity: input_select.marstek_m6_phase + name: Phase - type: vertical-stack cards: - type: markdown diff --git a/home assistant/packages/house_battery_control.yaml b/home assistant/packages/house_battery_control.yaml index eb3e011..3e32c36 100644 --- a/home assistant/packages/house_battery_control.yaml +++ b/home assistant/packages/house_battery_control.yaml @@ -388,6 +388,17 @@ input_number: mode: box icon: mdi:home-export-outline + # Peak shaving | per-phase threshold for custom/future phase-aware strategies + house_battery_control_max_phase_power: + name: Max phase power + min: 500 + max: 25000 + initial: 5500 + step: 1 + unit_of_measurement: "W" + mode: box + icon: mdi:transmission-tower + # Strategy charge | desired energy reserve (effective energy available excl. cut-off energies) # old name: house_battery_strategy_timed_target_energy house_battery_strategy_charge_target_energy: @@ -659,6 +670,41 @@ input_select: - Full control # Full control over the battery, no Marstek control icon: mdi:battery-heart + # Battery phase assignments + marstek_m1_phase: + name: "Marstek M1 Phase" + options: &BatteryPhaseOptions + - Unassigned + - L1 + - L2 + - L3 + icon: mdi:transmission-tower + + marstek_m2_phase: + name: "Marstek M2 Phase" + options: *BatteryPhaseOptions + icon: mdi:transmission-tower + + marstek_m3_phase: + name: "Marstek M3 Phase" + options: *BatteryPhaseOptions + icon: mdi:transmission-tower + + marstek_m4_phase: + name: "Marstek M4 Phase" + options: *BatteryPhaseOptions + icon: mdi:transmission-tower + + marstek_m5_phase: + name: "Marstek M5 Phase" + options: *BatteryPhaseOptions + icon: mdi:transmission-tower + + marstek_m6_phase: + name: "Marstek M6 Phase" + options: *BatteryPhaseOptions + icon: mdi:transmission-tower + # Master strategy house_battery_strategy: name: House Battery Strategy @@ -868,6 +914,93 @@ template: {# Marstek uses negative power when delivering power, positive when charging. This is inverse from what HA expects.#} {{ (total_power * -1) | round(2) }} + # Live battery power on phase L1 + - name: "House Battery L1 Power" + unique_id: "house_battery_l1_power_kw" + state_class: measurement + device_class: "power" + unit_of_measurement: "kW" + icon: mdi:transmission-tower + state: > + {% set total_w = namespace(value=0) %} + {% if states('input_select.marstek_m1_phase') == 'L1' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m1_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m2_phase') == 'L1' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m2_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m3_phase') == 'L1' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m3_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m4_phase') == 'L1' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m4_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m5_phase') == 'L1' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m5_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m6_phase') == 'L1' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m6_battery_power') | float(0)) %} + {% endif %} + {{ ((total_w.value * -1) / 1000) | round(2) }} + + # Live battery power on phase L2 + - name: "House Battery L2 Power" + unique_id: "house_battery_l2_power_kw" + state_class: measurement + device_class: "power" + unit_of_measurement: "kW" + icon: mdi:transmission-tower + state: > + {% set total_w = namespace(value=0) %} + {% if states('input_select.marstek_m1_phase') == 'L2' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m1_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m2_phase') == 'L2' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m2_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m3_phase') == 'L2' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m3_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m4_phase') == 'L2' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m4_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m5_phase') == 'L2' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m5_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m6_phase') == 'L2' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m6_battery_power') | float(0)) %} + {% endif %} + {{ ((total_w.value * -1) / 1000) | round(2) }} + + # Live battery power on phase L3 + - name: "House Battery L3 Power" + unique_id: "house_battery_l3_power_kw" + state_class: measurement + device_class: "power" + unit_of_measurement: "kW" + icon: mdi:transmission-tower + state: > + {% set total_w = namespace(value=0) %} + {% if states('input_select.marstek_m1_phase') == 'L3' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m1_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m2_phase') == 'L3' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m2_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m3_phase') == 'L3' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m3_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m4_phase') == 'L3' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m4_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m5_phase') == 'L3' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m5_battery_power') | float(0)) %} + {% endif %} + {% if states('input_select.marstek_m6_phase') == 'L3' %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m6_battery_power') | float(0)) %} + {% endif %} + {{ ((total_w.value * -1) / 1000) | round(2) }} + # Time Remaining - name: "Battery time remaining" unique_id: house_battery_time_remaining diff --git a/node-red/01 start-flow.json b/node-red/01 start-flow.json index 3f899e2..7c9a3de 100644 --- a/node-red/01 start-flow.json +++ b/node-red/01 start-flow.json @@ -675,7 +675,7 @@ "z": "cf69560481408644", "g": "98643475a97c956a", "name": "Mapping", - "func": "// Map to batteries array\nmsg.batteries.push({\n id: `M${msg.battery_index}`,\n power: parseInt(msg.battery_power,10),\n charging_max: parseInt(msg.max_charge_power,10),\n discharging_max: parseInt(msg.max_discharge_power,10),\n soc: parseInt(msg.battery_state_of_charge,10),\n soc_max: parseInt(msg.charging_cutoff_capacity,10),\n soc_min: parseInt(msg.discharging_cutoff_capacity,10),\n inverter: String(msg.inverter_state),\n energy: Number(msg.battery_remaining_capacity),\n energy_max: Number(msg.battery_total_energy),\n rs485: String(msg.rs485_control_mode)\n });\n\n// Continue\nreturn msg;", + "func": "// Normalize optional phase assignment\nconst normalizedPhase = String(msg.battery_phase || \"\").trim().toUpperCase();\nconst phase = [\"L1\", \"L2\", \"L3\"].includes(normalizedPhase) ? normalizedPhase : \"unassigned\";\n\n// Map to batteries array\nmsg.batteries.push({\n id: `M${msg.battery_index}`,\n phase: phase,\n power: parseInt(msg.battery_power,10),\n charging_max: parseInt(msg.max_charge_power,10),\n discharging_max: parseInt(msg.max_discharge_power,10),\n soc: parseInt(msg.battery_state_of_charge,10),\n soc_max: parseInt(msg.charging_cutoff_capacity,10),\n soc_min: parseInt(msg.discharging_cutoff_capacity,10),\n inverter: String(msg.inverter_state),\n energy: Number(msg.battery_remaining_capacity),\n energy_max: Number(msg.battery_total_energy),\n rs485: String(msg.rs485_control_mode)\n });\n\n// Continue\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -1932,7 +1932,7 @@ "y": 500, "wires": [ [ - "43bc77e1a735d1d4" + "9eaf5b01ad50102a" ] ] }, @@ -1942,7 +1942,7 @@ "z": "cf69560481408644", "g": "98643475a97c956a", "name": "Loop end", - "func": "// cleanup \ndelete msg.battery_index;\n\ndelete msg.battery_power;\ndelete msg.max_charge_power;\ndelete msg.max_discharge_power;\ndelete msg.battery_state_of_charge;\ndelete msg.charging_cutoff_capacity;\ndelete msg.discharging_cutoff_capacity;\ndelete msg.inverter_state;\ndelete msg.battery_remaining_capacity;\ndelete msg.battery_total_energy;\ndelete msg.rs485_control_mode;\n\nreturn msg;", + "func": "// cleanup \ndelete msg.battery_index;\n\ndelete msg.battery_power;\ndelete msg.max_charge_power;\ndelete msg.max_discharge_power;\ndelete msg.battery_state_of_charge;\ndelete msg.charging_cutoff_capacity;\ndelete msg.discharging_cutoff_capacity;\ndelete msg.inverter_state;\ndelete msg.battery_remaining_capacity;\ndelete msg.battery_total_energy;\ndelete msg.rs485_control_mode;\ndelete msg.battery_phase;\ndelete msg.template;\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -3262,7 +3262,7 @@ "y": 1040, "wires": [ [ - "f7e09807696115eb" + "9eaf5b01ad50102c" ] ] }, @@ -3567,5 +3567,89 @@ "modules": { "node-red-contrib-home-assistant-websocket": "0.80.3" } + }, + { + "id": "9eaf5b01ad50102a", + "type": "function", + "z": "cf69560481408644", + "g": "2d7842e57b2a0b38", + "name": "Phase template", + "func": "// Prepare optional battery phase helper lookup\nmsg.template = `{{ states('input_select.marstek_m${msg.battery_index}_phase') }}`;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1760, + "y": 500, + "wires": [ + [ + "9eaf5b01ad50102b" + ] + ] + }, + { + "id": "9eaf5b01ad50102b", + "type": "api-render-template", + "z": "cf69560481408644", + "g": "2d7842e57b2a0b38", + "name": "Phase", + "server": "176d29a.6f648d6", + "version": 0, + "template": "", + "resultsLocation": "battery_phase", + "resultsLocationType": "msg", + "templateLocation": "", + "templateLocationType": "none", + "x": 1920, + "y": 500, + "wires": [ + [ + "43bc77e1a735d1d4" + ] + ] + }, + { + "id": "9eaf5b01ad50102c", + "type": "api-render-template", + "z": "cf69560481408644", + "g": "80405532eb3f52d4", + "name": "Max phase power", + "server": "176d29a.6f648d6", + "version": 0, + "template": "{{ states('input_number.house_battery_control_max_phase_power') | float(5500) }}", + "resultsLocation": "grid_power_limit_phase", + "resultsLocationType": "msg", + "templateLocation": "", + "templateLocationType": "none", + "x": 880, + "y": 1040, + "wires": [ + [ + "9eaf5b01ad50102d" + ] + ] + }, + { + "id": "9eaf5b01ad50102d", + "type": "function", + "z": "cf69560481408644", + "g": "80405532eb3f52d4", + "name": "Normalize phase limit", + "func": "const phaseLimit = Number(RED.util.getMessageProperty(msg, \"grid_power_limit_phase\"));\nRED.util.setMessageProperty(\n msg,\n \"grid_power_limit_phase\",\n Number.isFinite(phaseLimit) && phaseLimit > 0 ? phaseLimit : 5500,\n true\n);\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1080, + "y": 1040, + "wires": [ + [ + "f7e09807696115eb" + ] + ] } -] \ No newline at end of file +] diff --git a/node-red/all-flows-in-one-file.json b/node-red/all-flows-in-one-file.json index 2ffe90b..7cbed2a 100644 --- a/node-red/all-flows-in-one-file.json +++ b/node-red/all-flows-in-one-file.json @@ -3109,7 +3109,7 @@ "z": "419f395a5f52024b", "g": "a7c766e9d6b9fce6", "name": "Mapping", - "func": "// Map to batteries array\nmsg.batteries.push({\n id: `M${msg.battery_index}`,\n power: parseInt(msg.battery_power,10),\n charging_max: parseInt(msg.max_charge_power,10),\n discharging_max: parseInt(msg.max_discharge_power,10),\n soc: parseInt(msg.battery_state_of_charge,10),\n soc_max: parseInt(msg.charging_cutoff_capacity,10),\n soc_min: parseInt(msg.discharging_cutoff_capacity,10),\n inverter: String(msg.inverter_state),\n energy: Number(msg.battery_remaining_capacity),\n energy_max: Number(msg.battery_total_energy),\n rs485: String(msg.rs485_control_mode)\n });\n\n// Continue\nreturn msg;", + "func": "// Normalize optional phase assignment\nconst normalizedPhase = String(msg.battery_phase || \"\").trim().toUpperCase();\nconst phase = [\"L1\", \"L2\", \"L3\"].includes(normalizedPhase) ? normalizedPhase : \"unassigned\";\n\n// Map to batteries array\nmsg.batteries.push({\n id: `M${msg.battery_index}`,\n phase: phase,\n power: parseInt(msg.battery_power,10),\n charging_max: parseInt(msg.max_charge_power,10),\n discharging_max: parseInt(msg.max_discharge_power,10),\n soc: parseInt(msg.battery_state_of_charge,10),\n soc_max: parseInt(msg.charging_cutoff_capacity,10),\n soc_min: parseInt(msg.discharging_cutoff_capacity,10),\n inverter: String(msg.inverter_state),\n energy: Number(msg.battery_remaining_capacity),\n energy_max: Number(msg.battery_total_energy),\n rs485: String(msg.rs485_control_mode)\n });\n\n// Continue\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4366,7 +4366,7 @@ "y": 500, "wires": [ [ - "fea927e3388ed2c9" + "9eaf5b01ad50102a" ] ] }, @@ -4376,7 +4376,7 @@ "z": "419f395a5f52024b", "g": "a7c766e9d6b9fce6", "name": "Loop end", - "func": "// cleanup \ndelete msg.battery_index;\n\ndelete msg.battery_power;\ndelete msg.max_charge_power;\ndelete msg.max_discharge_power;\ndelete msg.battery_state_of_charge;\ndelete msg.charging_cutoff_capacity;\ndelete msg.discharging_cutoff_capacity;\ndelete msg.inverter_state;\ndelete msg.battery_remaining_capacity;\ndelete msg.battery_total_energy;\ndelete msg.rs485_control_mode;\n\nreturn msg;", + "func": "// cleanup \ndelete msg.battery_index;\n\ndelete msg.battery_power;\ndelete msg.max_charge_power;\ndelete msg.max_discharge_power;\ndelete msg.battery_state_of_charge;\ndelete msg.charging_cutoff_capacity;\ndelete msg.discharging_cutoff_capacity;\ndelete msg.inverter_state;\ndelete msg.battery_remaining_capacity;\ndelete msg.battery_total_energy;\ndelete msg.rs485_control_mode;\ndelete msg.battery_phase;\ndelete msg.template;\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5696,7 +5696,7 @@ "y": 1040, "wires": [ [ - "2c2ed88ff158d0b7" + "9eaf5b01ad50102c" ] ] }, @@ -16037,5 +16037,89 @@ "x": 165, "y": 320, "wires": [] + }, + { + "id": "9eaf5b01ad50102a", + "type": "function", + "z": "419f395a5f52024b", + "g": "261b10d232e1c04e", + "name": "Phase template", + "func": "// Prepare optional battery phase helper lookup\nmsg.template = `{{ states('input_select.marstek_m${msg.battery_index}_phase') }}`;\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1760, + "y": 500, + "wires": [ + [ + "9eaf5b01ad50102b" + ] + ] + }, + { + "id": "9eaf5b01ad50102b", + "type": "api-render-template", + "z": "419f395a5f52024b", + "g": "261b10d232e1c04e", + "name": "Phase", + "server": "176d29a.6f648d6", + "version": 0, + "template": "", + "resultsLocation": "battery_phase", + "resultsLocationType": "msg", + "templateLocation": "", + "templateLocationType": "none", + "x": 1920, + "y": 500, + "wires": [ + [ + "fea927e3388ed2c9" + ] + ] + }, + { + "id": "9eaf5b01ad50102c", + "type": "api-render-template", + "z": "419f395a5f52024b", + "g": "79e4b4f8fef66565", + "name": "Max phase power", + "server": "176d29a.6f648d6", + "version": 0, + "template": "{{ states('input_number.house_battery_control_max_phase_power') | float(5500) }}", + "resultsLocation": "grid_power_limit_phase", + "resultsLocationType": "msg", + "templateLocation": "", + "templateLocationType": "none", + "x": 880, + "y": 1040, + "wires": [ + [ + "9eaf5b01ad50102d" + ] + ] + }, + { + "id": "9eaf5b01ad50102d", + "type": "function", + "z": "419f395a5f52024b", + "g": "79e4b4f8fef66565", + "name": "Normalize phase limit", + "func": "const phaseLimit = Number(RED.util.getMessageProperty(msg, \"grid_power_limit_phase\"));\nRED.util.setMessageProperty(\n msg,\n \"grid_power_limit_phase\",\n Number.isFinite(phaseLimit) && phaseLimit > 0 ? phaseLimit : 5500,\n true\n);\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1080, + "y": 1040, + "wires": [ + [ + "2c2ed88ff158d0b7" + ] + ] } -] \ No newline at end of file +] From ba1fad7262ae22a8346fcc18cf82a7497a7d9ff2 Mon Sep 17 00:00:00 2001 From: nickles-lee Date: Wed, 3 Jun 2026 13:33:11 +0200 Subject: [PATCH 2/5] Expose per-phase info from meter --- docs/06-advanced-features.md | 2 + home assistant/dashboard.yaml | 14 +----- .../packages/house_battery_control.yaml | 5 ++ .../house_battery_control_config.yaml | 25 ++++++++++ node-red/01 start-flow.json | 50 ++++++++++++++++++- node-red/all-flows-in-one-file.json | 50 ++++++++++++++++++- 6 files changed, 129 insertions(+), 17 deletions(-) diff --git a/docs/06-advanced-features.md b/docs/06-advanced-features.md index caa3988..860d925 100644 --- a/docs/06-advanced-features.md +++ b/docs/06-advanced-features.md @@ -67,6 +67,8 @@ nav_order: 6 - **Manual phase assignment:** Assign each configured battery to `L1`, `L2`, `L3`, or `Unassigned` from the dashboard. - The overview shows the assigned phase on each battery header and shows live battery power per phase in kW. - The Node-RED battery object exposes this as `battery.phase`, so custom strategies can use the mapping. + - Optional per-phase grid power aliases can be configured in `packages/house_battery_control_config.yaml` as `sensor.p1_meter_l1_power`, `sensor.p1_meter_l2_power`, and `sensor.p1_meter_l3_power`. Leave them commented out if your setup is not three-phase. + - Node-RED exposes configured phase meter values as `msg.grid_power_phase.L1`, `.L2`, and `.L3`, with missing or unreadable values set to `null`. - Built-in strategies still use aggregate control by default; per-phase control/peak shaving is not enforced yet. - **3-Phase self-consumption:** if you require 0 W grid consumption on a per phase basis, the setup changes slightly. diff --git a/home assistant/dashboard.yaml b/home assistant/dashboard.yaml index 8266b53..08ddd08 100644 --- a/home assistant/dashboard.yaml +++ b/home assistant/dashboard.yaml @@ -410,7 +410,7 @@ views: columns: full rows: 1 - type: heading - heading: Phase power + heading: Battery Phase Interaction heading_style: subtitle icon: mdi:transmission-tower - type: glance @@ -436,8 +436,6 @@ views: name: M1 SoC - entity: sensor.marstek_m1_ac_power name: M1 Power - - entity: input_select.marstek_m1_phase - name: Phase - entity: number.marstek_m1_max_charge_power name: Max Charge - entity: number.marstek_m1_max_discharge_power @@ -465,8 +463,6 @@ views: name: M2 SoC - entity: sensor.marstek_m2_ac_power name: M2 Power - - entity: input_select.marstek_m2_phase - name: Phase - entity: number.marstek_m2_max_charge_power name: Max Charge - entity: number.marstek_m2_max_discharge_power @@ -499,8 +495,6 @@ views: name: M3 SoC - entity: sensor.marstek_m3_ac_power name: M3 Power - - entity: input_select.marstek_m3_phase - name: Phase - entity: number.marstek_m3_max_charge_power name: Max Charge - entity: number.marstek_m3_max_discharge_power @@ -533,8 +527,6 @@ views: name: M4 SoC - entity: sensor.marstek_m4_ac_power name: M4 Power - - entity: input_select.marstek_m4_phase - name: Phase - entity: number.marstek_m4_max_charge_power name: Max Charge - entity: number.marstek_m4_max_discharge_power @@ -567,8 +559,6 @@ views: name: M5 SoC - entity: sensor.marstek_m5_ac_power name: M5 Power - - entity: input_select.marstek_m5_phase - name: Phase - entity: number.marstek_m5_max_charge_power name: Max Charge - entity: number.marstek_m5_max_discharge_power @@ -601,8 +591,6 @@ views: name: M6 SoC - entity: sensor.marstek_m6_ac_power name: M6 Power - - entity: input_select.marstek_m6_phase - name: Phase - entity: number.marstek_m6_max_charge_power name: Max Charge - entity: number.marstek_m6_max_discharge_power diff --git a/home assistant/packages/house_battery_control.yaml b/home assistant/packages/house_battery_control.yaml index 3e32c36..2980520 100644 --- a/home assistant/packages/house_battery_control.yaml +++ b/home assistant/packages/house_battery_control.yaml @@ -914,6 +914,11 @@ template: {# Marstek uses negative power when delivering power, positive when charging. This is inverse from what HA expects.#} {{ (total_power * -1) | round(2) }} + # Battery-side phase interaction totals. + # Optional grid-side phase meter aliases are configured in + # house_battery_control_config.yaml as sensor.p1_meter_l1_power, + # sensor.p1_meter_l2_power and sensor.p1_meter_l3_power. + # Live battery power on phase L1 - name: "House Battery L1 Power" unique_id: "house_battery_l1_power_kw" diff --git a/home assistant/packages/house_battery_control_config.yaml b/home assistant/packages/house_battery_control_config.yaml index 7572993..8385ef5 100644 --- a/home assistant/packages/house_battery_control_config.yaml +++ b/home assistant/packages/house_battery_control_config.yaml @@ -36,3 +36,28 @@ template: unit_of_measurement: "W" device_class: "power" state_class: "measurement" + + # House battery control | Optional per-phase grid power + # Uncomment these aliases if your three-phase meter exposes net power per phase. + # Positive = import from grid, negative = export to grid. + # Leave these blocks commented out if you do not have a three-phase setup. + # - name: "P1 Meter L1 Power" + # unique_id: "p1_meter_l1_power" + # state: "{{ states('sensor.your_l1_phase_power_sensor') }}" # << SET YOUR L1 PHASE POWER SENSOR HERE + # unit_of_measurement: "W" + # device_class: "power" + # state_class: "measurement" + + # - name: "P1 Meter L2 Power" + # unique_id: "p1_meter_l2_power" + # state: "{{ states('sensor.your_l2_phase_power_sensor') }}" # << SET YOUR L2 PHASE POWER SENSOR HERE + # unit_of_measurement: "W" + # device_class: "power" + # state_class: "measurement" + + # - name: "P1 Meter L3 Power" + # unique_id: "p1_meter_l3_power" + # state: "{{ states('sensor.your_l3_phase_power_sensor') }}" # << SET YOUR L3 PHASE POWER SENSOR HERE + # unit_of_measurement: "W" + # device_class: "power" + # state_class: "measurement" diff --git a/node-red/01 start-flow.json b/node-red/01 start-flow.json index 7c9a3de..d5b19fe 100644 --- a/node-red/01 start-flow.json +++ b/node-red/01 start-flow.json @@ -317,11 +317,15 @@ "f6f3ce29833ec4cf", "5ac3cdf0ad6013e3", "69d4aa99b46127ba", - "783deb36dab080de" + "783deb36dab080de", + "9eaf5b01ad50102c", + "9eaf5b01ad50102d", + "9eaf5b01ad50102e", + "9eaf5b01ad50102f" ], "x": 34, "y": 999, - "w": 752, + "w": 1642, "h": 82 }, { @@ -3646,6 +3650,48 @@ "libs": [], "x": 1080, "y": 1040, + "wires": [ + [ + "9eaf5b01ad50102e" + ] + ] + }, + { + "id": "9eaf5b01ad50102e", + "type": "api-render-template", + "z": "cf69560481408644", + "g": "80405532eb3f52d4", + "name": "Phase grid power", + "server": "176d29a.6f648d6", + "version": 0, + "template": "{%- macro phase_power(entity_id) -%}\n{%- set raw = states(entity_id) -%}\n{%- if raw in ['unknown', 'unavailable', 'none', 'None', ''] -%}\nnull\n{%- elif is_number(raw) -%}\n{{ raw | float }}\n{%- else -%}\nnull\n{%- endif -%}\n{%- endmacro -%}\n{\"L1\": {{ phase_power('sensor.p1_meter_l1_power') }}, \"L2\": {{ phase_power('sensor.p1_meter_l2_power') }}, \"L3\": {{ phase_power('sensor.p1_meter_l3_power') }}}", + "resultsLocation": "grid_power_phase", + "resultsLocationType": "msg", + "templateLocation": "", + "templateLocationType": "none", + "x": 1290, + "y": 1040, + "wires": [ + [ + "9eaf5b01ad50102f" + ] + ] + }, + { + "id": "9eaf5b01ad50102f", + "type": "function", + "z": "cf69560481408644", + "g": "80405532eb3f52d4", + "name": "Normalize phase power", + "func": "let phasePower = RED.util.getMessageProperty(msg, \"grid_power_phase\");\n\nif (typeof phasePower === \"string\") {\n const rawPhasePower = phasePower.trim();\n if (rawPhasePower.length > 0) {\n try {\n phasePower = JSON.parse(rawPhasePower);\n } catch (error) {\n phasePower = {};\n }\n } else {\n phasePower = {};\n }\n}\n\nif (!phasePower || typeof phasePower !== \"object\") {\n phasePower = {};\n}\n\nconst toNumberOrNull = (value) => {\n const numericValue = Number(value);\n return Number.isFinite(numericValue) ? numericValue : null;\n};\n\nRED.util.setMessageProperty(msg, \"grid_power_phase\", {\n L1: toNumberOrNull(phasePower.L1 ?? phasePower.l1),\n L2: toNumberOrNull(phasePower.L2 ?? phasePower.l2),\n L3: toNumberOrNull(phasePower.L3 ?? phasePower.l3),\n}, true);\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1510, + "y": 1040, "wires": [ [ "f7e09807696115eb" diff --git a/node-red/all-flows-in-one-file.json b/node-red/all-flows-in-one-file.json index 7cbed2a..52e2c6c 100644 --- a/node-red/all-flows-in-one-file.json +++ b/node-red/all-flows-in-one-file.json @@ -1342,11 +1342,15 @@ "a6203334d934a28a", "d8d5fbfc33432345", "783d88edb917dab9", - "f496519e76d1c4fa" + "f496519e76d1c4fa", + "9eaf5b01ad50102c", + "9eaf5b01ad50102d", + "9eaf5b01ad50102e", + "9eaf5b01ad50102f" ], "x": 34, "y": 999, - "w": 752, + "w": 1642, "h": 82 }, { @@ -16116,6 +16120,48 @@ "libs": [], "x": 1080, "y": 1040, + "wires": [ + [ + "9eaf5b01ad50102e" + ] + ] + }, + { + "id": "9eaf5b01ad50102e", + "type": "api-render-template", + "z": "419f395a5f52024b", + "g": "79e4b4f8fef66565", + "name": "Phase grid power", + "server": "176d29a.6f648d6", + "version": 0, + "template": "{%- macro phase_power(entity_id) -%}\n{%- set raw = states(entity_id) -%}\n{%- if raw in ['unknown', 'unavailable', 'none', 'None', ''] -%}\nnull\n{%- elif is_number(raw) -%}\n{{ raw | float }}\n{%- else -%}\nnull\n{%- endif -%}\n{%- endmacro -%}\n{\"L1\": {{ phase_power('sensor.p1_meter_l1_power') }}, \"L2\": {{ phase_power('sensor.p1_meter_l2_power') }}, \"L3\": {{ phase_power('sensor.p1_meter_l3_power') }}}", + "resultsLocation": "grid_power_phase", + "resultsLocationType": "msg", + "templateLocation": "", + "templateLocationType": "none", + "x": 1290, + "y": 1040, + "wires": [ + [ + "9eaf5b01ad50102f" + ] + ] + }, + { + "id": "9eaf5b01ad50102f", + "type": "function", + "z": "419f395a5f52024b", + "g": "79e4b4f8fef66565", + "name": "Normalize phase power", + "func": "let phasePower = RED.util.getMessageProperty(msg, \"grid_power_phase\");\n\nif (typeof phasePower === \"string\") {\n const rawPhasePower = phasePower.trim();\n if (rawPhasePower.length > 0) {\n try {\n phasePower = JSON.parse(rawPhasePower);\n } catch (error) {\n phasePower = {};\n }\n } else {\n phasePower = {};\n }\n}\n\nif (!phasePower || typeof phasePower !== \"object\") {\n phasePower = {};\n}\n\nconst toNumberOrNull = (value) => {\n const numericValue = Number(value);\n return Number.isFinite(numericValue) ? numericValue : null;\n};\n\nRED.util.setMessageProperty(msg, \"grid_power_phase\", {\n L1: toNumberOrNull(phasePower.L1 ?? phasePower.l1),\n L2: toNumberOrNull(phasePower.L2 ?? phasePower.l2),\n L3: toNumberOrNull(phasePower.L3 ?? phasePower.l3),\n}, true);\n\nreturn msg;", + "outputs": 1, + "timeout": 0, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1510, + "y": 1040, "wires": [ [ "2c2ed88ff158d0b7" From cb7e37a43318a627c8ec45618c125c4165eb28e6 Mon Sep 17 00:00:00 2001 From: nickles-lee Date: Wed, 3 Jun 2026 14:33:44 +0200 Subject: [PATCH 3/5] Hide phase interaction when no phases assigned --- home assistant/dashboard.yaml | 8 ++++++++ home assistant/packages/house_battery_control.yaml | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/home assistant/dashboard.yaml b/home assistant/dashboard.yaml index 08ddd08..1189274 100644 --- a/home assistant/dashboard.yaml +++ b/home assistant/dashboard.yaml @@ -413,6 +413,10 @@ views: heading: Battery Phase Interaction heading_style: subtitle icon: mdi:transmission-tower + visibility: + - condition: state + entity: binary_sensor.house_battery_has_phase_assignment + state: 'on' - type: glance entities: - entity: sensor.house_battery_l1_power @@ -422,6 +426,10 @@ views: - entity: sensor.house_battery_l3_power name: L3 columns: 3 + visibility: + - condition: state + entity: binary_sensor.house_battery_has_phase_assignment + state: 'on' - type: markdown content: >- ### Marstek M1{% set phase = states('input_select.marstek_m1_phase') %}{% if phase == 'L1' %} - Phase 1{% elif phase == 'L2' %} - Phase 2{% elif phase == 'L3' %} - Phase 3{% endif %} diff --git a/home assistant/packages/house_battery_control.yaml b/home assistant/packages/house_battery_control.yaml index 2980520..dd669f5 100644 --- a/home assistant/packages/house_battery_control.yaml +++ b/home assistant/packages/house_battery_control.yaml @@ -1097,6 +1097,18 @@ template: {{ [max_available - filled, 0] | max | round(2) if max_available > 0 else 0 }} - binary_sensor: + # Battery phase assignments | has any battery been assigned to a phase? + - name: "House Battery Has Phase Assignment" + unique_id: house_battery_has_phase_assignment + icon: mdi:transmission-tower + state: > + {{ states('input_select.marstek_m1_phase') in ['L1', 'L2', 'L3'] or + states('input_select.marstek_m2_phase') in ['L1', 'L2', 'L3'] or + states('input_select.marstek_m3_phase') in ['L1', 'L2', 'L3'] or + states('input_select.marstek_m4_phase') in ['L1', 'L2', 'L3'] or + states('input_select.marstek_m5_phase') in ['L1', 'L2', 'L3'] or + states('input_select.marstek_m6_phase') in ['L1', 'L2', 'L3'] }} + # Strategy dynamic | is the avg tariff low enough to call this a 'cheap period' - name: "Is below threshold cheapest period tariff" unique_id: house_battery_strategy_dynamic_is_below_threshold_cheapest_tariff From b37c8da8dc4f317b18e7c2fd921f4c2bc54e6e15 Mon Sep 17 00:00:00 2001 From: nickles-lee Date: Wed, 3 Jun 2026 14:38:12 +0200 Subject: [PATCH 4/5] Remove tracked macOS metadata --- .gitignore | 3 ++- home assistant/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 home assistant/.DS_Store diff --git a/.gitignore b/.gitignore index 3565fd3..57472ed 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,11 @@ RELEASE_NOTES_TEMP.txt # Ignore notes and development files notes/ template_sensor_bob_battery_dashboard.yaml +.DS_Store # Ignore SASS cache files docs/.sass-cache # Claude Code per-user state — keep local; commands/skills/agents/settings.json are shared .claude/settings.local.json -.claude/sessions/ \ No newline at end of file +.claude/sessions/ diff --git a/home assistant/.DS_Store b/home assistant/.DS_Store deleted file mode 100644 index 8c1d6d6615afb76919813ad015c0434f8bdb3985..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5FM8;D!yH^3`gh~8IpbDr0 zzfA#NyT`PkB~`Sh_WGUE9a`(Wo)ww31s3@3z~X)~o!PQ#G2A(DY11?-Y=-#J;NtD- zIe9tno1?V(w{xn8&i^_I2wfR4b+bU z>Px)y5*CLj-4x1sjiU z>p6fnbdnkJwmXKRNXpS2PE7S6_T mjYpS)LXKls;iLEz?hJhqSAel#;}I<|{Sa_6XrT)Hr~>bHo{Db( From 5b85367a4f345b40fa883ab74d629b540965a0ef Mon Sep 17 00:00:00 2001 From: nickles-lee Date: Sat, 20 Jun 2026 15:20:30 +0200 Subject: [PATCH 5/5] improve .gitignore + switch power reference --- .gitignore | 9 +++- docs/06-advanced-features.md | 2 +- .../packages/house_battery_control.yaml | 44 +++++++++---------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 57472ed..adf601a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,16 @@ notes/ template_sensor_bob_battery_dashboard.yaml .DS_Store -# Ignore SASS cache files +# Ignore opencode local workspace +.opencode/ + docs/.sass-cache +# Node.js dependencies and test output +node_modules/ + +coverage/ + # Claude Code per-user state — keep local; commands/skills/agents/settings.json are shared .claude/settings.local.json .claude/sessions/ diff --git a/docs/06-advanced-features.md b/docs/06-advanced-features.md index 860d925..9f474c1 100644 --- a/docs/06-advanced-features.md +++ b/docs/06-advanced-features.md @@ -65,7 +65,7 @@ nav_order: 6 - **More than 6 batteries:** Override or change `input_number.house_battery_count` and you are good to go. - The dashboard supports 6 batteries out of the box. For 7 or more, duplicate and edit these cards or create your own dashboard. - **Manual phase assignment:** Assign each configured battery to `L1`, `L2`, `L3`, or `Unassigned` from the dashboard. - - The overview shows the assigned phase on each battery header and shows live battery power per phase in kW. + - The overview shows the assigned phase on each battery header and shows live battery AC power per phase in kW. - The Node-RED battery object exposes this as `battery.phase`, so custom strategies can use the mapping. - Optional per-phase grid power aliases can be configured in `packages/house_battery_control_config.yaml` as `sensor.p1_meter_l1_power`, `sensor.p1_meter_l2_power`, and `sensor.p1_meter_l3_power`. Leave them commented out if your setup is not three-phase. - Node-RED exposes configured phase meter values as `msg.grid_power_phase.L1`, `.L2`, and `.L3`, with missing or unreadable values set to `null`. diff --git a/home assistant/packages/house_battery_control.yaml b/home assistant/packages/house_battery_control.yaml index dd669f5..4661961 100644 --- a/home assistant/packages/house_battery_control.yaml +++ b/home assistant/packages/house_battery_control.yaml @@ -914,12 +914,12 @@ template: {# Marstek uses negative power when delivering power, positive when charging. This is inverse from what HA expects.#} {{ (total_power * -1) | round(2) }} - # Battery-side phase interaction totals. + # AC-side phase interaction totals. # Optional grid-side phase meter aliases are configured in # house_battery_control_config.yaml as sensor.p1_meter_l1_power, # sensor.p1_meter_l2_power and sensor.p1_meter_l3_power. - # Live battery power on phase L1 + # Live AC power on phase L1 - name: "House Battery L1 Power" unique_id: "house_battery_l1_power_kw" state_class: measurement @@ -929,26 +929,26 @@ template: state: > {% set total_w = namespace(value=0) %} {% if states('input_select.marstek_m1_phase') == 'L1' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m1_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m1_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m2_phase') == 'L1' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m2_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m2_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m3_phase') == 'L1' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m3_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m3_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m4_phase') == 'L1' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m4_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m4_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m5_phase') == 'L1' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m5_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m5_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m6_phase') == 'L1' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m6_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m6_ac_power') | float(0)) %} {% endif %} {{ ((total_w.value * -1) / 1000) | round(2) }} - # Live battery power on phase L2 + # Live AC power on phase L2 - name: "House Battery L2 Power" unique_id: "house_battery_l2_power_kw" state_class: measurement @@ -958,26 +958,26 @@ template: state: > {% set total_w = namespace(value=0) %} {% if states('input_select.marstek_m1_phase') == 'L2' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m1_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m1_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m2_phase') == 'L2' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m2_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m2_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m3_phase') == 'L2' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m3_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m3_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m4_phase') == 'L2' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m4_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m4_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m5_phase') == 'L2' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m5_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m5_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m6_phase') == 'L2' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m6_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m6_ac_power') | float(0)) %} {% endif %} {{ ((total_w.value * -1) / 1000) | round(2) }} - # Live battery power on phase L3 + # Live AC power on phase L3 - name: "House Battery L3 Power" unique_id: "house_battery_l3_power_kw" state_class: measurement @@ -987,22 +987,22 @@ template: state: > {% set total_w = namespace(value=0) %} {% if states('input_select.marstek_m1_phase') == 'L3' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m1_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m1_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m2_phase') == 'L3' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m2_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m2_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m3_phase') == 'L3' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m3_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m3_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m4_phase') == 'L3' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m4_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m4_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m5_phase') == 'L3' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m5_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m5_ac_power') | float(0)) %} {% endif %} {% if states('input_select.marstek_m6_phase') == 'L3' %} - {% set total_w.value = total_w.value + (states('sensor.marstek_m6_battery_power') | float(0)) %} + {% set total_w.value = total_w.value + (states('sensor.marstek_m6_ac_power') | float(0)) %} {% endif %} {{ ((total_w.value * -1) / 1000) | round(2) }}