From dfcfbc26a7128bcd556b5f629762545a940f15af Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 07:47:48 +0000 Subject: [PATCH] Add rule1_time_window and rule1_weekday auto modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new opt-in select entities (default: manual) that let the user delegate management of the two rule 1 registers the integration was previously leaving alone: - rule1_time_window: auto → writes start_time=00:00, stop_time=23:59 (Felicity's 24-hour convention; firmware rejects stop=00:00 or stop=24:00). - rule1_weekday: auto → writes effective_week=0x7F (all 7 days). Implementation: - coordinator._apply_rule1_auto_settings() runs every cycle. Writes are idempotent: it reads the current register value back from self.data and only writes when the value differs from the target. After a successful write, self.data is updated locally so subsequent ticks immediately see the match and skip. - Called from the main update loop after _calculate_available_info so the warning check (_check_rule1_window_conflict) sees the corrected state in the same cycle. - Default is "manual" so existing user-configured restrictions are preserved unless the user explicitly opts into auto. This complements the rule1_window_warning (advisory banner) — users who don't want to manage rule 1's window themselves can flip both selects to auto and the warning will resolve automatically as the integration brings the registers into sync. --- CLAUDE.md | 26 +++++++-- custom_components/ha_felicity/__init__.py | 2 + custom_components/ha_felicity/coordinator.py | 61 +++++++++++++++++++- custom_components/ha_felicity/select.py | 30 ++++++++++ 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a717d04..a98650e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,12 +89,24 @@ The integration controls the inverter via **Economic Rule 1** Modbus registers. 2. Every 10 seconds, the coordinator checks if the current slot is in the schedule 3. If the desired state differs from current state, it writes registers -**Rule 1 registers written**: `econ_rule_1_enable` (0/1/2), `_voltage`, -`_soc`, `_power`, `_start_day`, `_stop_day`. **NOT written**: -`econ_rule_1_start_time`, `_stop_time`, `_effective_week`. If those are -restricted on the inverter, it silently ignores our enable command when -the current time/weekday is outside the window — the EMS plans to act, -writes the register, and nothing happens. +**Rule 1 registers always written by `_transition_to_state`**: +`econ_rule_1_enable` (0/1/2), `_voltage`, `_soc`, `_power`, `_start_day`, +`_stop_day`. + +**Rule 1 registers written only when auto mode is enabled** (via +`_apply_rule1_auto_settings`, called every cycle, idempotent — only +writes when the register already differs from the target): +- `rule1_time_window=auto` → `econ_rule_1_start_time=00:00`, + `econ_rule_1_stop_time=23:59` (Felicity's 24-hour convention; the + firmware doesn't accept stop=00:00 or stop=24:00). +- `rule1_weekday=auto` → `econ_rule_1_effective_week=0x7F` (all 7 days). + +Both default to `manual` so the integration doesn't touch user-set +values unless they explicitly opt in. When still on `manual` and the +inverter's rule 1 window is restrictive, the inverter silently ignores +the enable command outside the window — the EMS plans to act, writes +the register, and nothing happens. The `rule1_window_warning` check +surfaces this in the EMS card. **Rule 1 window warning** (`coordinator._check_rule1_window_conflict`): runs every cycle. Builds the set of intended action slots (auto mode: @@ -396,6 +408,8 @@ Fetches `energy_state` history from HA API (throttled 60s), shows what actually | discharge_min_voltage | number | 48-55 V | 50 | Discharge voltage floor | | charge_to_full_on_negative_price | select | off/on | off | Charge at every p<0 slot (revenue) | | discharge_to_make_room_for_negative_price | select | off/on | off | Pre-discharge before p<0 PV windows | +| rule1_time_window | select | manual/auto | manual | Auto writes rule 1 start=00:00, stop=23:59 | +| rule1_weekday | select | manual/auto | manual | Auto writes rule 1 effective_week=all days | All `HA_FelicityInternalNumber` configuration entities render as input boxes (`NumberMode.BOX`) so users can type precise values. diff --git a/custom_components/ha_felicity/__init__.py b/custom_components/ha_felicity/__init__.py index 1686130..d2043fb 100644 --- a/custom_components/ha_felicity/__init__.py +++ b/custom_components/ha_felicity/__init__.py @@ -193,6 +193,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "safe_power_management": "auto", "charge_to_full_on_negative_price": "off", "discharge_to_make_room_for_negative_price": "off", + "rule1_time_window": "manual", + "rule1_weekday": "manual", CONF_REGISTER_SET: DEFAULT_REGISTER_SET, "update_interval": 10, } diff --git a/custom_components/ha_felicity/coordinator.py b/custom_components/ha_felicity/coordinator.py index d310a9d..2e7a695 100644 --- a/custom_components/ha_felicity/coordinator.py +++ b/custom_components/ha_felicity/coordinator.py @@ -1643,6 +1643,57 @@ async def _transition_to_state(self, new_state: str) -> bool: await self.TypeSpecificHandler.write_type_specific_register("econ_rule_1_power", target_watts) return True + async def _apply_rule1_auto_settings(self) -> None: + """If rule 1 auto settings are enabled, ensure the inverter's + time-window and weekday-mask match the auto defaults. + + Writes only when the current register value doesn't match the + target — so this is safe to call on every state activation. + Felicity's 24-hour convention is start=00:00, stop=23:59 (the + firmware doesn't accept stop=00:00 or stop=24:00). + """ + opts = self.config_entry.options + + if opts.get("rule1_time_window", "manual") == "auto": + target_start = 0 # 00:00 + target_stop = (23 << 8) | 59 # 23:59 (Felicity 24h) + current_start = self.data.get("econ_rule_1_start_time") + current_stop = self.data.get("econ_rule_1_stop_time") + if current_start != target_start: + ok = await self.TypeSpecificHandler.write_type_specific_register( + "econ_rule_1_start_time", target_start + ) + if ok: + self.data["econ_rule_1_start_time"] = target_start + _LOGGER.info( + "Rule 1 auto: wrote start_time=00:00 (was %s)", + current_start, + ) + if current_stop != target_stop: + ok = await self.TypeSpecificHandler.write_type_specific_register( + "econ_rule_1_stop_time", target_stop + ) + if ok: + self.data["econ_rule_1_stop_time"] = target_stop + _LOGGER.info( + "Rule 1 auto: wrote stop_time=23:59 (was %s)", + current_stop, + ) + + if opts.get("rule1_weekday", "manual") == "auto": + target_week = 0x7F # all 7 days enabled (bit0=Sun..bit6=Sat) + current_week = self.data.get("econ_rule_1_effective_week") + if current_week != target_week: + ok = await self.TypeSpecificHandler.write_type_specific_register( + "econ_rule_1_effective_week", target_week + ) + if ok: + self.data["econ_rule_1_effective_week"] = target_week + _LOGGER.info( + "Rule 1 auto: wrote effective_week=all days (was 0x%02X)", + current_week if isinstance(current_week, int) else 0, + ) + def get_energy_state_info(self) -> dict: """Get current energy management state info (useful for debugging sensor).""" @@ -1878,10 +1929,16 @@ def get_attr(names): # Always calculate available info (visible in both modes) self._calculate_available_info(battery_soc) + # Apply rule 1 time-window / weekday auto settings + # if enabled. Writes are idempotent — only happens + # when the register doesn't already match the target. + await self._apply_rule1_auto_settings() + # Warn if the planned schedule falls outside the # inverter's Economic Rule 1 time/weekday window - # (we don't write those registers, so the inverter - # would silently ignore our enable command there). + # (we don't write those registers in manual mode, + # so the inverter would silently ignore our enable + # command there). self.rule1_window_warning = self._check_rule1_window_conflict() # Update new_data with all results diff --git a/custom_components/ha_felicity/select.py b/custom_components/ha_felicity/select.py index ce20652..8713990 100644 --- a/custom_components/ha_felicity/select.py +++ b/custom_components/ha_felicity/select.py @@ -107,6 +107,36 @@ async def async_setup_entry( ) ) + # Rule 1 time-window auto management: on "auto" the integration writes + # start_time=00:00 and stop_time=23:59 (Felicity's 24h convention) so + # the rule never gates the schedule on time of day. + entities.append( + HA_FelicitySpecialModeSelect( + coordinator=coordinator, + entry=entry, + option_key="rule1_time_window", + select_options=["manual", "auto"], + name="Rule 1 Time Window", + icon="mdi:clock-outline", + entity_category=EntityCategory.CONFIG, + ) + ) + + # Rule 1 weekday-mask auto management: on "auto" the integration writes + # effective_week=0x7F (all 7 days) so the rule never gates the schedule + # on day of week. + entities.append( + HA_FelicitySpecialModeSelect( + coordinator=coordinator, + entry=entry, + option_key="rule1_weekday", + select_options=["manual", "auto"], + name="Rule 1 Weekday", + icon="mdi:calendar-week", + entity_category=EntityCategory.CONFIG, + ) + ) + # Tie all entities to the device for entity in entities: entity._attr_device_info = device_info