Skip to content

Commit f4f60d1

Browse files
committed
Merge origin/main: resolve conflicts with rule1 auto modes + negative-price strategies
Conflicts in __init__.py, coordinator.py, select.py resolved by keeping both sides: - Our branch: optimization_priority, block_export_on_negative_price, battery_cycle_cost_eur_kwh UI entities + upgrade defaults - Main: charge_to_full_on_negative_price, discharge_to_make_room, rule1_time_window, rule1_weekday entities + defaults block_export_on_negative_price retains our str→bool conversion (tolerates legacy bool and new "on"/"off" select values). 158 tests pass (10 new from main). https://claude.ai/code/session_01P9LAQ5ET6SU9dLWULxv2uF
2 parents 9a46608 + 8901973 commit f4f60d1

18 files changed

Lines changed: 1238 additions & 82 deletions

CLAUDE.md

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,36 @@ The integration controls the inverter via **Economic Rule 1** Modbus registers.
8989
2. Every 10 seconds, the coordinator checks if the current slot is in the schedule
9090
3. If the desired state differs from current state, it writes registers
9191

92+
**Rule 1 registers always written by `_transition_to_state`**:
93+
`econ_rule_1_enable` (0/1/2), `_voltage`, `_soc`, `_power`, `_start_day`,
94+
`_stop_day`.
95+
96+
**Rule 1 registers written only when auto mode is enabled** (via
97+
`_apply_rule1_auto_settings`, called every cycle, idempotent — only
98+
writes when the register already differs from the target):
99+
- `rule1_time_window=auto``econ_rule_1_start_time=00:00`,
100+
`econ_rule_1_stop_time=23:59` (Felicity's 24-hour convention; the
101+
firmware doesn't accept stop=00:00 or stop=24:00).
102+
- `rule1_weekday=auto``econ_rule_1_effective_week=0x7F` (all 7 days).
103+
104+
Both default to `manual` so the integration doesn't touch user-set
105+
values unless they explicitly opt in. When still on `manual` and the
106+
inverter's rule 1 window is restrictive, the inverter silently ignores
107+
the enable command outside the window — the EMS plans to act, writes
108+
the register, and nothing happens. The `rule1_window_warning` check
109+
surfaces this in the EMS card.
110+
111+
**Rule 1 window warning** (`coordinator._check_rule1_window_conflict`):
112+
runs every cycle. Builds the set of intended action slots (auto mode:
113+
`scheduled_slots` + tomorrow; manual mode: today's remaining slots
114+
crossing the threshold in the active direction) and checks each against
115+
the rule 1 time-of-day window and effective-weekday mask read back from
116+
the inverter. `start_time == stop_time` is treated as "all day"; a full
117+
0x7F mask as "all days". Any mismatch is exposed as the
118+
`rule1_window_warning` attribute on `schedule_status` and rendered as a
119+
banner in the EMS card. Weekday bit mapping: inverter bit0=Sunday..
120+
bit6=Saturday, mapped from Python via `isoweekday() % 7`.
121+
92122
**Charge deferral** (`coordinator._determine_energy_state`): when the current
93123
slot is flagged `charge` but a later scheduled charge slot has a cheaper
94124
price (non-negative slots only), execution returns `idle` instead. The next
@@ -150,6 +180,8 @@ This prevents the SOC prediction from assuming unrealistic charge rates
150180
| battery_cycle_cost_eur_kwh | 0.0 | Wear cost; added to min sell price |
151181
| optimization_priority | "cost" | cost / longevity / self_consumption |
152182
| block_export_on_negative_price | True | Skip sell scheduling at p < 0 |
183+
| charge_to_full_on_negative_price | False | Schedule every p<0 slot (revenue at p<0); accepts forced PV curtailment |
184+
| discharge_to_make_room_for_negative_price | False | Pre-emptively discharge before p<0 PV windows so PV can fill the battery |
153185

154186
The coordinator applies a SOH factor to nominal `battery_capacity_kwh`
155187
before constructing `EMSConfig` — ems.py treats the capacity as
@@ -267,6 +299,49 @@ max_today_slots = floor(headroom / effective_per_slot)
267299
# SOC validation prunes only when PV alone wouldn't fill the battery.
268300
```
269301

302+
### Negative-Price Strategies (charge_to_full / discharge_to_make_room)
303+
304+
Two orthogonal opt-in flags that change how negative-price slots are
305+
handled. Both are off by default and compose with all grid modes
306+
(from_grid, to_grid, both).
307+
308+
**`charge_to_full_on_negative_price`** — acts *during* p<0 slots
309+
- In `_schedule_from_grid` / `_schedule_both`, after the normal
310+
cheapest-slot selection, every negative-price slot in the remaining
311+
window is added to the charge set (deduplicated).
312+
- `_validate_schedule_soc` is called with `keep_all_negative_charges=True`.
313+
When set, negative-price slots are exempt from overflow pruning even
314+
when PV alone wouldn't fill the battery (the legacy `pv_fills_battery`
315+
exemption is broadened to "all negatives").
316+
- Phantom-charge slots (battery already at capacity entering the slot)
317+
are kept too — the inverter may try to charge and the BMS will gate
318+
it. No harm; the schedule reflects user intent.
319+
- Trade-off: the user accepts some forced PV curtailment in exchange
320+
for guaranteed revenue at every p<0 slot.
321+
322+
**`discharge_to_make_room_for_negative_price`** — acts *before* p<0 slots
323+
- New helper `_select_discharges_for_pv_headroom` runs after charge
324+
selection. Walks the SOC trajectory forward; whenever a negative-
325+
price slot with PV surplus would overflow the battery, schedules
326+
discharge in the *most expensive* earlier positive-price slot to
327+
create headroom.
328+
- Validity: SOC must never drop below the absolute `min_kwh` floor
329+
(hardware safety), and end-of-day SOC must remain >= reserve_target
330+
(overnight coverage). Temporary dips below reserve during the day
331+
are allowed — the negative-window PV will refill the battery.
332+
- Works in `from_grid` mode (which normally never discharges) as well
333+
as `to_grid` / `both`. In `both` mode, the make-room discharges
334+
are merged with the regular sell-side selection (sells take
335+
precedence on conflicting slots).
336+
337+
**Composition**: when both flags are on, make-room discharges are
338+
scheduled before negative windows, then charge slots fire during the
339+
negatives. Net effect: maximum profit on negative-price days
340+
(discharge at peak + buy at p<0 + PV fills the cleared battery).
341+
342+
Exposed as off/on select entities (`HA_FelicitySpecialModeSelect`)
343+
in the UI.
344+
270345
---
271346

272347
## Safe Power Management (coordinator.py `_check_safe_power`, ~1307-1424)
@@ -341,6 +416,17 @@ Fetches `energy_state` history from HA API (throttled 60s), shows what actually
341416
| max_amperage_per_phase | number | 10-63 A | 16 | Grid current limit |
342417
| voltage_level | number | 48-60 V | 58 | Charge voltage setpoint |
343418
| discharge_min_voltage | number | 48-55 V | 50 | Discharge voltage floor |
419+
| charge_to_full_on_negative_price | select | off/on | off | Charge at every p<0 slot (revenue) |
420+
| discharge_to_make_room_for_negative_price | select | off/on | off | Pre-discharge before p<0 PV windows |
421+
| rule1_time_window | select | manual/auto | manual | Auto writes rule 1 start=00:00, stop=23:59 |
422+
| rule1_weekday | select | manual/auto | manual | Auto writes rule 1 effective_week=all days |
423+
424+
All `HA_FelicityInternalNumber` configuration entities render as
425+
input boxes (`NumberMode.BOX`) so users can type precise values.
426+
Sliders are awkward for fractional / fine-grained settings like
427+
`efficiency_factor` (step 0.01) or `arbitrage_price_delta` (step
428+
0.01 €/kWh). Pass `mode=NumberMode.SLIDER` to the constructor for
429+
entities that benefit from scrubbing.
344430

345431
### Key Sensor Entities
346432

@@ -383,8 +469,22 @@ The confidence factor can drop to 0.1 early in the day on partially cloudy morni
383469
### 4. Consumption Estimate Sensitivity
384470
The algorithm uses consumption_est/24 for hourly drain — assumes flat consumption. Houses with evening peaks (cooking, heating) may see under-predicted evening drain.
385471

386-
### 5. Anti-Conflict Guard Only Checks Instantaneous Power
387-
The 200W grid import check (suppress discharge when importing) triggers on instantaneous reading. Short spikes (kettle, microwave) can briefly suppress profitable discharge.
472+
### 5. Anti-Conflict Guard Hysteresis — IMPLEMENTED
473+
Previously the 200W grid import check suppressed discharge on a single
474+
tick, causing flipper behaviour (discharge → idle → discharge every
475+
~16s) on short load spikes (kettle, microwave, EV start). Two writes
476+
per flip is brutal on the inverter and customers notice.
477+
478+
Now uses thresholded hysteresis:
479+
- Small/moderate import (200–2000W) must persist for ≥ 2 consecutive
480+
cycles (≈ 32s) before suppression triggers.
481+
- Large import (> 2000W) suppresses immediately (genuine sustained
482+
draw like EV charging or oven preheat).
483+
- After suppression ends, a 60-second cooldown blocks re-suppression
484+
so the inverter stabilises before the next decision.
485+
- Each cycle now logs the grid_power + state decision at DEBUG level
486+
so the flipper pattern is easy to spot in retrospect:
487+
`State decision: desired=X, current=Y, soc=%, price=, threshold=, grid_power=W`
388488

389489
### 6. Generator-Port Solar Workaround
390490
TREX-25/50 with micro-inverters on the generator port need special handling. PV registers read 0, falling back to generator_day_cost_energy. Both backend and frontend handle this but it's fragile.
@@ -398,6 +498,17 @@ hour slot. This was especially painful at midnight when stale
398498
`state.state` still reports the previous day's stale total, the coordinator
399499
falls back to the filtered hourly sum.
400500

501+
### 8. Midnight should not force inverter to idle
502+
The day-rollover block in `_async_update_data` used to unconditionally
503+
call `_transition_to_state("idle")` and then skip the normal cycle for
504+
that tick. This cancelled valid charge/discharge actions that should
505+
continue across midnight (e.g., a customer selling overnight to clear
506+
the battery before negative-midday PV refills it next day). The block
507+
now does only the bookkeeping (yesterday deficit, daily consumption,
508+
SOC history reset, slot overrides rotation) and falls through to the
509+
normal cycle, which re-determines the desired state and only writes a
510+
transition if the state actually changes.
511+
401512
---
402513

403514
## Algorithm Assessment and Improvement Recommendations

Installation description.pdf

1.16 MB
Binary file not shown.

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
[![HACS validated](https://img.shields.io/badge/HACS-validated-41BDF5?style=flat-square)](https://github.com/hacs/integration)
77

88
Felicity inverter home assistant integration for easy setup and use of the device (via [Modbus](https://www.se.com/us/en/faqs/FA168406/)).
9+
Additionally includes a full Energy Management System to Buy and Sell electricity on the best moments and guards maximum power use to save guard against overcurrent.
10+
With the right settings the software makes sure you pay the best energy prices or use no grid if not needed.
911

1012
For this integration to work you need to have a wired modbus connection to your inverter either [via this USB dongle](https://www.amazon.nl/Industrial-Converter-Lightningproof-Resettable-Protection/dp/B0B87YJLJQ?source=ps-sl-shoppingads-lpcontext&ref_=fplfs&psc=1&smid=A2FQD9ZIAONBLW) or via something [like this](https://www.kiwi-electronics.com/nl/rs485-to-rj45-ethernet-tcp-ip-to-serial-rail-mount-support-20109?country=NL&utm_term=20109&gad_source=1&gad_campaignid=19763718639&gbraid=0AAAAADuMvucKntnrNZrVkZAHDgps81zYC&gclid=Cj0KCQiAx8PKBhD1ARIsAKsmGbeFZaWC_S38eFyu1NtZ0SP4zyLWwMWG70BRz6Ur1nmBymMCxvSR1_kaAmR9EALw_wcB).
1113
Currently supports IVGM / TREX types:
@@ -58,8 +60,10 @@ It supports modbus USB dongle and TCP [Modbus](https://www.se.com/us/en/faqs/FA1
5860
The 3 possible ways are explained in the picture below. At the moment the last part always requires a RS485 connection to the inverter.
5961
<p align="center">
6062
<img src="https://github.com/partach/ha_felicity/blob/main/pictures/HA-felicity-connect.png" width="600"/>
61-
<br><em>Ways to connect the inverter</em>
63+
<img src="https://github.com/partach/ha_felicity/blob/main/pictures/modbus_location_trex10k.png" width="400"/>
64+
<br><em>Ways to connect the inverter and TREX10k modbus location</em>
6265
</p>
66+
NOTE: when using the USR-D164 wifi module you need to put Pack Interval to 100 (20 causes packet loss)
6367

6468
## Installation options
6569
The T-REX 5 and 10K series with HP or HL (High / Low Voltage batteries) with 1 or 3 Phases (P1 or P3) can be selected with selecting
@@ -83,7 +87,9 @@ This can be done via configuration when the intallation is succesfull (device fo
8387
## Configuration
8488
After successfull install the integration can be configured at any time with a few settings. See picture on top for location of the gear icon
8589
- Update interval (the frequency of refresh of data). For the T-REX 5-10k models keep it on 10 sec minimum due to small baud rate.
86-
- Monetary override. Nordpool is supported by default but also other monetary integrations as Tibber. The format is that it needs a sensor with attributes about min, max, avg price
90+
- Setting up Nordpool (For energy prices). Use the HACS version, NOT the default version. (HACS version has 15 min slot information). You need to setup Nordpool for your energy supplier, see the web for examples.
91+
- Solar Forecast for today and tomorrow. Install an integration that predicts solar power. It should support a Today and Tomorrow sensor showing total expected amount (you need to configure the solar forecast right).
92+
- Monetary override. If you use Nordpool (recommended) leave this empty! Nordpool is supported by default but also other monetary integrations as Tibber. The format is that it needs a sensor with attributes about min, max, avg price
8793
If you want use Tibber enter in the override fied: `sensor.tibber_electricity_price` where electricity_price is the sensor with attributes (avg, min, max) and 'tibber' how you named the integration.
8894
The Felicity integration looks for a variaty of avg_price like fields as attributes and if it finds in the the override sensor, uses that as needed price information. If no information is found,
8995
the price information remains unavailable.
@@ -111,7 +117,7 @@ Note1:
111117
Note2: The Operating mode **must be set (by user) to Economic mode**. The Energy management feature will not engage in any other mode (Like General).
112118

113119
During setup or with config setting (gear symbol in hub/device overview) you can add a 'Monetary' Home Assistant Device.
114-
Examples are the Nordpool integration or Tibber. Look at the Nordpool integration details on how to set that up (not covered here).
120+
Examples are the Nordpool integration (HACS version only, not default) or Another. Look at the HACS Nordpool integration details on how to set that up (not covered here).
115121
During first setup or during run-time configuration (device gear symbol) it will display a list of installed Monetary integrations to chose from.
116122
Currently Nordpool and Tibber (via Norpool override field in config) are tested to work.
117123

@@ -128,30 +134,33 @@ Example: Max price = 0.30 Euro, Min Price = 0.20 Euro and Avergage Price = 0.25
128134
When setting the `Price Threshold Level to 5` the Base-Threshold-Price will be 0.25.
129135

130136
**The Grid Mode setting**:
131-
* If `Grid Mode` <em>(From-grid, To-Grid, Off)</em> is set to From-grid it will allow use of grid power when actual price is <=0.25 Euro
132-
* If `Grid Mode` <em>(From-grid, To-Grid, Off)</em> is set to To-grid it will allow Battery power to go to grid power when actual price is >=0.25 Euro
137+
* If `Grid Mode` <em>(From-grid, To-Grid, Both, Off)</em> is set to From-grid it will allow use of grid power when actual price is <=0.25 Euro (as per given example)
138+
* If `Grid Mode` <em>(From-grid, To-Grid, Both, Off)</em> is set to To-grid it will allow Battery power to go to grid power when actual price is >=0.25 Euro (as per given example)
133139
**Additional variables** are `Battery Charge Max Level` and `Battery Charge Min Level`.
134140
* In `From Grid mode` it will stop when `Actual Battery Capacity` reaches `Battery Charge Max Level`
135141
* In `To Grid mode` it will stop when `Actual Battery Capacity` reaches `Battery Charge Min Level`
142+
* In `Both` it will sell and charge the battery optimally to make the least amount of cost / use the grid as less as possible
136143

137-
IMPORTANT: The integration is depedent on the Monetary Integration to contiously supply the data.
144+
IMPORTANT: The integration is depedent on the Monetary Integration and Solar Forecast Integration to contiously supply the data.
138145

139146
## Dynamic Power Management
140147
The integration also supports Dynamic Power Management. After instalation, via configuration entities (see above picture), you can set the maximum amperage of your home electricity setup.
141148
For example if you have a maximum of 16A per group, set the value to 16A. The integration will then make sure the battery loading will be dialed back if the amperage becomes to high.
142149
(by decreasing the user requested power level, controlled via rule 1 via the integration).
143150
It will keep monitorning this and will increase the battery loading to requested power levels if the amperage becomes lower.
144151

145-
## Using the card
152+
## Using the cards
146153
After installation of the integration you need to first reboot HA.
147-
The card will be automatically installed and registered by the integration on start up.
154+
The cards will be automatically installed and registered by the integration on start up.
148155
To use the card in your dashboard, go to you dashboard, edit, choose `Add card`.
149-
Choose `Manual`
150-
Add first line: `type: custom:felicity-inverter-card`
156+
They can be found at the bottom of the list.
157+
If they are not visible you can choose `Manual` as card type.
158+
Add first line: `type: custom:felicity-inverter-card` for the inverter card and `type: custom:felicity-ems-card` for the EMS card.
151159
Then choose the `visual editor` to continue.
152-
From the `Device` dropdown chose your felicity inverter install.
160+
From the `Device` dropdown chose your felicity inverter integration installed.
153161
<p align="center">
154162
<img src="https://github.com/partach/ha_felicity/blob/main/pictures/HA-felicity-card-expl.png" width="600"/>
163+
<img src="https://github.com/partach/ha_felicity/blob/main/pictures/ems_ui_explanation.png" width="600"/>
155164
<br><em>Card usage explained</em>
156165
</p>
157166

custom_components/ha_felicity/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
197197
"battery_cycle_cost_eur_kwh": 0.0,
198198
"optimization_priority": "cost",
199199
"block_export_on_negative_price": "on",
200+
"charge_to_full_on_negative_price": "off",
201+
"discharge_to_make_room_for_negative_price": "off",
202+
"rule1_time_window": "manual",
203+
"rule1_weekday": "manual",
200204
CONF_REGISTER_SET: DEFAULT_REGISTER_SET,
201205
"update_interval": 10,
202206
}

0 commit comments

Comments
 (0)