Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions custom_components/ha_felicity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
61 changes: 59 additions & 2 deletions custom_components/ha_felicity/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions custom_components/ha_felicity/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading