Skip to content

Commit 9d57338

Browse files
authored
Merge pull request #134 from partach/claude/expand-felicity-card-B7dl4
Sanity check fixes: expose multi-objective knobs in UI, fix doc drift
2 parents 8901973 + f4f60d1 commit 9d57338

6 files changed

Lines changed: 80 additions & 16 deletions

File tree

CLAUDE.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,21 @@ the cheapest scheduled slot. This compensates for deficit shrinking as PV
127127
confidence recovers mid-day — without it, early expensive slots would
128128
execute while later cheaper slots got dropped from a re-plan.
129129

130-
### State Transitions (coordinator.py:1195-1231)
130+
### State Transitions (coordinator.py `_transition_to_state`, ~1425-1482)
131131

132132
| State | econ_rule_1_enable | Voltage | SOC | Power |
133133
|---|---|---|---|---|
134134
| charging | 1 | voltage_level (default 58V) | charge_max (default 100%) | safe_max_power (W) |
135-
| discharging | 2 | discharge_min_voltage (default 50V) | discharge_min (default 20%) | safe_max_power (W) |
135+
| discharging | 2 | discharge_min_voltage (default 50V) | discharge floor (see below) | safe_max_power (W) |
136136
| idle | 0 | *(not written)* | *(not written)* | *(not written)* |
137137

138+
**Discharge SOC floor**: in auto mode (grid_mode active) the discharging
139+
SOC register is written from the *computed* `_reserve_target_pct`
140+
(the planned reserve target), not the raw `battery_discharge_min_level`.
141+
This makes the inverter's hardware floor match the schedule's intended
142+
reserve rather than the absolute minimum. When the EMS is off it falls
143+
back to the user's `discharge_min` setting.
144+
138145
### Model Differences
139146

140147
| Aspect | TREX-5 | TREX-10 | TREX-25 | TREX-50 |
@@ -337,7 +344,7 @@ in the UI.
337344

338345
---
339346

340-
## Safe Power Management (coordinator.py:1077-1193)
347+
## Safe Power Management (coordinator.py `_check_safe_power`, ~1307-1424)
341348

342349
Monitors grid current per phase and adjusts inverter power:
343350

@@ -398,11 +405,14 @@ Fetches `energy_state` history from HA API (throttled 60s), shows what actually
398405
| price_threshold_level | number | 1-10 | 5 | Manual price level |
399406
| battery_charge_max_level | number | 30-100% | 100 | Max SOC for charging |
400407
| battery_discharge_min_level | number | 10-70% | 20 | Min SOC for discharging |
401-
| battery_capacity_kwh | number | 1-100 kWh | 10 | Usable battery capacity |
408+
| battery_capacity_kwh | number | 1-200 kWh | 10 | Usable battery capacity |
402409
| efficiency_factor | number | 0.70-1.00 | 0.90 | Round-trip efficiency |
403-
| daily_consumption_estimate | number | 0-100 kWh | 10 | Fallback consumption |
410+
| daily_consumption_estimate | number | 0-120 kWh | 10 | Fallback consumption |
404411
| reserve_target_pct | number | 0-100% | 0 | Fixed reserve floor (0=dynamic) |
405412
| arbitrage_price_delta | number | 0-0.50 €/kWh | 0 | Price spread for full charge |
413+
| battery_cycle_cost_eur_kwh | number | 0-0.50 €/kWh | 0 | Battery wear cost (profitability filter) |
414+
| optimization_priority | select | cost/longevity/self_consumption | cost | Multi-objective strategy |
415+
| block_export_on_negative_price | select | on/off | on | Skip sell scheduling at p < 0 |
406416
| max_amperage_per_phase | number | 10-63 A | 16 | Grid current limit |
407417
| voltage_level | number | 48-60 V | 58 | Charge voltage setpoint |
408418
| discharge_min_voltage | number | 48-55 V | 50 | Discharge voltage floor |
@@ -675,7 +685,23 @@ other entities) to be unusable on fresh installs.
675685
keys (`max_amperage_per_phase`, `safe_power_management`,
676686
`battery_cycle_cost_eur_kwh`, `optimization_priority`,
677687
`block_export_on_negative_price`) so new installations get them
678-
written at setup time.
688+
written at setup time. The migration block `defaults_to_set` in
689+
`__init__.py` (runs on every `async_setup_entry`) mirrors the same
690+
keys so *existing* installs upgrading to a newer version backfill
691+
missing options too.
692+
693+
#### C6. UI Entities for Multi-Objective Knobs — IMPLEMENTED
694+
The three knobs added in the lifecycle overhaul previously had no UI
695+
control — they lived only in `entry.options` defaults and could not be
696+
changed by the user. They now have entities:
697+
- `battery_cycle_cost_eur_kwh` → number (0-0.50 €/kWh, number.py)
698+
- `optimization_priority` → select (cost/longevity/self_consumption, select.py)
699+
- `block_export_on_negative_price` → select (on/off, select.py)
700+
701+
Because `block_export_on_negative_price` is stored as a string ("on"/"off")
702+
by the select but consumed as a bool by `EMSConfig`, the coordinator
703+
converts it: `str(opts.get(..., "on")).lower() not in ("off","false","0")`.
704+
This also tolerates the legacy bool value from earlier installs.
679705

680706
### Deferred Items (Need Separate Iteration)
681707

custom_components/ha_felicity/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
191191
"daily_consumption_estimate": 10,
192192
"price_mode": "manual",
193193
"safe_power_management": "auto",
194+
"reserve_target_pct": 0,
195+
"arbitrage_price_delta": 0.0,
196+
"max_amperage_per_phase": 16,
197+
"battery_cycle_cost_eur_kwh": 0.0,
198+
"optimization_priority": "cost",
199+
"block_export_on_negative_price": "on",
194200
"charge_to_full_on_negative_price": "off",
195201
"discharge_to_make_room_for_negative_price": "off",
196202
"rule1_time_window": "manual",

custom_components/ha_felicity/config_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def _get_default_options(self) -> dict:
9090
"safe_power_management": self._user_input.get("safe_power_management", "auto"),
9191
"battery_cycle_cost_eur_kwh": self._user_input.get("battery_cycle_cost_eur_kwh", 0.0),
9292
"optimization_priority": self._user_input.get("optimization_priority", "cost"),
93-
"block_export_on_negative_price": self._user_input.get("block_export_on_negative_price", True),
93+
"block_export_on_negative_price": self._user_input.get("block_export_on_negative_price", "on"),
9494
}
9595

9696
@staticmethod

custom_components/ha_felicity/coordinator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -688,9 +688,9 @@ def _calculate_schedule(self, battery_soc: float | None) -> None:
688688
arbitrage_price_delta=opts.get("arbitrage_price_delta", 0.0),
689689
battery_cycle_cost_eur_kwh=opts.get("battery_cycle_cost_eur_kwh", 0.0),
690690
optimization_priority=opts.get("optimization_priority", "cost"),
691-
block_export_on_negative_price=opts.get(
692-
"block_export_on_negative_price", True
693-
),
691+
block_export_on_negative_price=str(
692+
opts.get("block_export_on_negative_price", "on")
693+
).lower() not in ("off", "false", "0"),
694694
charge_to_full_on_negative_price=opts.get(
695695
"charge_to_full_on_negative_price", "off"
696696
) == "on",

custom_components/ha_felicity/number.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ async def async_setup_entry(
175175
step=5,
176176
unit="%",
177177
icon="mdi:battery-lock",
178+
default_value=0,
178179
),
179180
HA_FelicityInternalNumber(
180181
coordinator,
@@ -186,6 +187,19 @@ async def async_setup_entry(
186187
step=0.01,
187188
unit="€/kWh",
188189
icon="mdi:cash-multiple",
190+
default_value=0.0,
191+
),
192+
HA_FelicityInternalNumber(
193+
coordinator,
194+
entry,
195+
option_key="battery_cycle_cost_eur_kwh",
196+
name="Battery Cycle Cost",
197+
min_val=0,
198+
max_val=0.50,
199+
step=0.01,
200+
unit="€/kWh",
201+
icon="mdi:battery-clock",
202+
default_value=0.0,
189203
),
190204
])
191205

custom_components/ha_felicity/select.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,30 @@ async def async_setup_entry(
8383
)
8484
)
8585

86+
entities.append(
87+
HA_FelicitySpecialModeSelect(
88+
coordinator=coordinator,
89+
entry=entry,
90+
option_key="optimization_priority",
91+
select_options=["cost", "longevity", "self_consumption"],
92+
name="Optimization Priority",
93+
icon="mdi:scale-balance",
94+
entity_category=EntityCategory.CONFIG,
95+
)
96+
)
97+
98+
entities.append(
99+
HA_FelicitySpecialModeSelect(
100+
coordinator=coordinator,
101+
entry=entry,
102+
option_key="block_export_on_negative_price",
103+
select_options=["on", "off"],
104+
name="Block Export On Negative Price",
105+
icon="mdi:transmission-tower-export",
106+
entity_category=EntityCategory.CONFIG,
107+
)
108+
)
109+
86110
entities.append(
87111
HA_FelicitySpecialModeSelect(
88112
coordinator=coordinator,
@@ -107,9 +131,6 @@ async def async_setup_entry(
107131
)
108132
)
109133

110-
# Rule 1 time-window auto management: on "auto" the integration writes
111-
# start_time=00:00 and stop_time=23:59 (Felicity's 24h convention) so
112-
# the rule never gates the schedule on time of day.
113134
entities.append(
114135
HA_FelicitySpecialModeSelect(
115136
coordinator=coordinator,
@@ -122,9 +143,6 @@ async def async_setup_entry(
122143
)
123144
)
124145

125-
# Rule 1 weekday-mask auto management: on "auto" the integration writes
126-
# effective_week=0x7F (all 7 days) so the rule never gates the schedule
127-
# on day of week.
128146
entities.append(
129147
HA_FelicitySpecialModeSelect(
130148
coordinator=coordinator,

0 commit comments

Comments
 (0)