From fc3774a8dc23c14bf4654cb5b40bf20241e30820 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 1 Mar 2026 23:26:59 +1030 Subject: [PATCH 01/13] Improve chargers --- README.md | 13 ++ custom_components/alphaess/binary_sensor.py | 152 ++++++++++++++++++++ custom_components/alphaess/button.py | 21 +++ custom_components/alphaess/const.py | 1 + custom_components/alphaess/coordinator.py | 42 +++++- custom_components/alphaess/entity.py | 8 ++ custom_components/alphaess/enums.py | 2 + custom_components/alphaess/sensor.py | 31 +++- custom_components/alphaess/sensorlist.py | 26 +++- 9 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 custom_components/alphaess/binary_sensor.py diff --git a/README.md b/README.md index 49e4172..36a472a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,19 @@ An error will be placed in the logs The current charge config, discharge config and charging range will only update once the API is re-called (can be up to 1 min) +### EV charger controls + +The integration exposes EV charger controls (start/stop and current setting) when an EV charger is detected. + +- Start/stop commands are now validated against the current EV charger status before being sent. +- If a command is not valid for the current state, the integration skips the API call and logs a clear message. +- If notifications are enabled for that inverter, a persistent notification is shown when a command is rejected. +- Two EV diagnostic binary sensors are exposed: + - `Can Start Charging` + - `Can Stop Charging` + +These track whether a start/stop command is currently valid based on the latest EV charger status. + If you want to adjust the restrictions yourself, you are able to by modifying the `ALPHA_POST_REQUEST_RESTRICTION` varible in const.py to the amount of seconds allowed per call ## Local Inverter Support diff --git a/custom_components/alphaess/binary_sensor.py b/custom_components/alphaess/binary_sensor.py new file mode 100644 index 0000000..12961f6 --- /dev/null +++ b/custom_components/alphaess/binary_sensor.py @@ -0,0 +1,152 @@ +"""Binary sensor platform for AlphaESS integration.""" +from typing import List +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + DOMAIN, + CONF_SERIAL_NUMBER, + SUBENTRY_TYPE_INVERTER, + SUBENTRY_TYPE_EV_CHARGER, + CONF_PARENT_INVERTER, +) +from .coordinator import AlphaESSDataUpdateCoordinator +from .sensorlist import EV_CHARGER_BINARY_SENSORS +from .sensor import _build_ev_charger_device_info + +_LOGGER: logging.Logger = logging.getLogger(__package__) + + +async def async_setup_entry(hass, entry, async_add_entities) -> None: + """Set up EV charger readiness binary sensors.""" + coordinator: AlphaESSDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + ev_binary_supported_states = { + description.key: description for description in EV_CHARGER_BINARY_SENSORS + } + + for subentry in entry.subentries.values(): + if subentry.subentry_type == SUBENTRY_TYPE_INVERTER: + serial = subentry.data.get(CONF_SERIAL_NUMBER) + if not serial or serial not in coordinator.data: + continue + + data = coordinator.data[serial] + ev_charger = data.get("EV Charger S/N") + if not ev_charger: + continue + + ev_subentry_serials = { + sub.data.get(CONF_SERIAL_NUMBER) + for sub in entry.subentries.values() + if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER + } + if ev_charger in ev_subentry_serials: + continue + + ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_entities: List[BinarySensorEntity] = [] + for description in ev_binary_supported_states.values(): + ev_entities.append( + AlphaEVReadinessBinarySensor( + coordinator, + serial, + entry, + description, + ev_serial=ev_charger, + device_info=ev_device_info, + ) + ) + + if ev_entities: + async_add_entities(ev_entities, config_subentry_id=subentry.subentry_id) + + elif subentry.subentry_type == SUBENTRY_TYPE_EV_CHARGER: + parent_serial = subentry.data.get(CONF_PARENT_INVERTER) + if not parent_serial or parent_serial not in coordinator.data: + continue + + data = coordinator.data[parent_serial] + ev_charger = data.get("EV Charger S/N") + if not ev_charger: + continue + + ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_entities: List[BinarySensorEntity] = [] + for description in ev_binary_supported_states.values(): + ev_entities.append( + AlphaEVReadinessBinarySensor( + coordinator, + parent_serial, + entry, + description, + ev_serial=ev_charger, + device_info=ev_device_info, + ) + ) + + if ev_entities: + async_add_entities(ev_entities, config_subentry_id=subentry.subentry_id) + + +class AlphaEVReadinessBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Readiness sensor for EV charger start/stop commands.""" + + def __init__(self, coordinator, serial, config, description, ev_serial=None, device_info=None): + super().__init__(coordinator) + self._coordinator = coordinator + self._serial = serial + self._config = config + self._description = description + self._ev_serial = ev_serial + self._name = description.name + self._icon = description.icon + self._entity_category = description.entity_category + self._direction = description.direction + + if device_info: + self._attr_device_info = device_info + + @property + def is_on(self) -> bool | None: + """Return readiness to execute EV command.""" + if self._direction is None: + return None + + if self._coordinator.get_ev_charger_status_raw(self._serial) is None: + return None + + return self._coordinator.can_control_ev(self._serial, self._direction) + + @property + def available(self) -> bool: + """Readiness sensors require cloud EV data.""" + if not self.coordinator.last_update_success: + return False + if not self._coordinator.cloud_available: + return False + + serial_data = self._coordinator.data.get(self._serial, {}) + return serial_data.get("EV Charger S/N") is not None + + @property + def unique_id(self): + return f"{self._config.entry_id}_{self._serial} - {self._name}" + + @property + def name(self): + return f"{self._name}" + + @property + def suggested_object_id(self): + return f"{self._serial} {self._name}" + + @property + def entity_category(self): + return self._entity_category + + @property + def icon(self): + return self._icon diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index d1b6a1f..bf98b54 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -168,7 +168,23 @@ def _notifications_disabled(self) -> bool: async def async_press(self) -> None: + async def _notify_invalid_ev_command(action: str) -> None: + if not self._notifications_disabled: + await create_persistent_notification( + self.hass, + message=( + f"EV charger cannot {action.lower()} right now for {self._serial}. " + "Refresh and check EV Charger Status before retrying." + ), + title=f"{self._serial} EV Charger", + ) + if self._key == AlphaESSNames.stopcharging: + if not self._coordinator.can_control_ev(self._serial, 0): + _LOGGER.info("Stop charging ignored for %s due to EV state mismatch", self._serial) + await _notify_invalid_ev_command("Stop") + return + _LOGGER.info("Stopped charging") self._movement_state = None await self._coordinator.control_ev(self._serial, self._ev_serial, 0) @@ -179,6 +195,11 @@ async def async_press(self) -> None: return if self._key == AlphaESSNames.startcharging: + if not self._coordinator.can_control_ev(self._serial, 1): + _LOGGER.info("Start charging ignored for %s due to EV state mismatch", self._serial) + await _notify_invalid_ev_command("Start") + return + _LOGGER.info("started charging") self._movement_state = None await self._coordinator.control_ev(self._serial, self._ev_serial, 1) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index 40cf66b..6f82ace 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -6,6 +6,7 @@ DOMAIN = "alphaess" PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 69ba819..523f0fe 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -216,7 +216,9 @@ async def parse_power_data(self, power_data: Dict, one_day_power: Optional[list] # EV power data for i in range(1, 5): key_map = {1: "One", 2: "Two", 3: "Three", 4: "Four"} - data[getattr(AlphaESSNames, f"ElectricVehiclePower{key_map[i]}")] = await self.dp.safe_get(ev_details, f"ev{i}Power") + ev_power = await self.dp.safe_get(ev_details, f"ev{i}Power") + if ev_power is not None: + data[getattr(AlphaESSNames, f"ElectricVehiclePower{key_map[i]}")] = ev_power # Fallback SOC from daily data if one_day_power and soc == 0: @@ -370,8 +372,46 @@ async def set_ev_charger_current(self, serial: str, value: int) -> None: ) await self.async_request_refresh() + def get_ev_charger_status_raw(self, serial: str) -> int | None: + """Return EV charger raw status if available.""" + serial_data = self.data.get(serial, {}) + status = serial_data.get(AlphaESSNames.evchargerstatusraw) + if status is None: + status = serial_data.get(AlphaESSNames.evchargerstatus) + + try: + return int(status) if status is not None else None + except (TypeError, ValueError): + return None + + def can_control_ev(self, serial: str, direction: int) -> bool: + """Validate if EV remote command is compatible with current charger state. + + Direction: 0 = stop, 1 = start. + """ + status = self.get_ev_charger_status_raw(serial) + if status is None: + return False + + if direction == 1: + return status in (2, 4, 5) + if direction == 0: + return status in (3, 4, 5) + return False + async def control_ev(self, serial: str, ev_serial: str, direction: str) -> None: """Control EV charger.""" + parsed_direction = int(direction) + if not self.can_control_ev(serial, parsed_direction): + _LOGGER.warning( + "Skipping EV control command for %s (%s), direction=%s due to incompatible state=%s", + serial, + ev_serial, + direction, + self.get_ev_charger_status_raw(serial), + ) + return + result = await self.api.remoteControlEvCharger(serial, ev_serial, direction) _LOGGER.info( f"Control EV Charger: {ev_serial} for serial: {serial} " diff --git a/custom_components/alphaess/entity.py b/custom_components/alphaess/entity.py index 963099f..9dd75fa 100644 --- a/custom_components/alphaess/entity.py +++ b/custom_components/alphaess/entity.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from homeassistant.components.button import ButtonEntityDescription +from homeassistant.components.binary_sensor import BinarySensorEntityDescription from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.switch import SwitchEntityDescription @@ -29,6 +30,13 @@ class AlphaESSButtonDescription(ButtonEntityDescription): ] | None = lambda val: val +@dataclass(frozen=True) +class AlphaESSBinarySensorDescription(BinarySensorEntityDescription): + """Class to describe an AlphaESS Binary Sensor.""" + + direction: int | None = None + + @dataclass(frozen=True) class AlphaESSNumberDescription(NumberEntityDescription): """Class to describe an AlphaESS Number.""" diff --git a/custom_components/alphaess/enums.py b/custom_components/alphaess/enums.py index be6c275..45071bf 100644 --- a/custom_components/alphaess/enums.py +++ b/custom_components/alphaess/enums.py @@ -79,6 +79,8 @@ class AlphaESSNames(str, Enum): EVChargerCurrentSetting = "EV Charger Current Setting" startcharging = "Start Charging" stopcharging = "Stop Charging" + canstartcharging = "Can Start Charging" + canstopcharging = "Can Stop Charging" GridChargeEnabled = "Grid Charge Enabled" DischargeTimeControlEnabled = "Discharge Time Control Enabled" TodayGeneration = "Today's Generation" diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 1f03181..2bb8e79 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -21,6 +21,25 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) +EV_RELATED_KEYS = { + AlphaESSNames.evchargersn, + AlphaESSNames.evchargermodel, + AlphaESSNames.evchargerstatus, + AlphaESSNames.evchargerstatusraw, + AlphaESSNames.pev, + AlphaESSNames.ElectricVehiclePowerOne, + AlphaESSNames.ElectricVehiclePowerTwo, + AlphaESSNames.ElectricVehiclePowerThree, + AlphaESSNames.ElectricVehiclePowerFour, +} + +EV_CONNECTOR_POWER_KEYS = { + AlphaESSNames.ElectricVehiclePowerOne, + AlphaESSNames.ElectricVehiclePowerTwo, + AlphaESSNames.ElectricVehiclePowerThree, + AlphaESSNames.ElectricVehiclePowerFour, +} + def _build_inverter_device_info( coordinator: AlphaESSDataUpdateCoordinator, @@ -265,6 +284,13 @@ def available(self) -> bool: serial_data = self._coordinator.data.get(self._serial) if serial_data is None: return False + + if self._key in EV_RELATED_KEYS and serial_data.get(AlphaESSNames.evchargersn) is None: + return False + + if self._key in EV_CONNECTOR_POWER_KEYS: + return serial_data.get(self._key) is not None + return self._key in serial_data @property @@ -278,7 +304,10 @@ def native_value(self) -> StateType: raw_state = self._coordinator.data.get(self._serial, {}).get(self._key) if raw_state is None: return None - return EV_CHARGER_STATE_KEYS.get(raw_state, "unknown") + try: + return EV_CHARGER_STATE_KEYS.get(int(raw_state), "unknown") + except (TypeError, ValueError): + return "unknown" # Handle integer-mapped status sensors _STATUS_LOOKUPS = { diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 495ae15..275ef2f 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -8,7 +8,14 @@ from homeassistant.components.number import NumberMode -from .entity import AlphaESSSensorDescription, AlphaESSButtonDescription, AlphaESSNumberDescription, AlphaESSSwitchDescription, AlphaESSTimeDescription +from .entity import ( + AlphaESSSensorDescription, + AlphaESSButtonDescription, + AlphaESSBinarySensorDescription, + AlphaESSNumberDescription, + AlphaESSSwitchDescription, + AlphaESSTimeDescription, +) from .enums import AlphaESSNames FULL_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ @@ -869,6 +876,23 @@ ] +EV_CHARGER_BINARY_SENSORS: List[AlphaESSBinarySensorDescription] = [ + AlphaESSBinarySensorDescription( + key=AlphaESSNames.canstartcharging, + name="Can Start Charging", + icon="mdi:play-circle-outline", + entity_category=EntityCategory.DIAGNOSTIC, + direction=1, + ), + AlphaESSBinarySensorDescription( + key=AlphaESSNames.canstopcharging, + name="Can Stop Charging", + icon="mdi:stop-circle-outline", + entity_category=EntityCategory.DIAGNOSTIC, + direction=0, + ), +] + LOCAL_IP_SYSTEM_SENSORS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( key=AlphaESSNames.localIP, From 609b3d237948756ccf6b79b77c15858d8bef33c5 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 1 Mar 2026 23:32:00 +1030 Subject: [PATCH 02/13] Add SMILE-EVCS7 as a known inverter --- custom_components/alphaess/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index 6f82ace..9cda368 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -30,7 +30,7 @@ KNOWN_INVERTERS = ["Storion-S5", "SMILE5-INV", "VT1000", "SMILE-T10-HV-INV", "SMILE-G3-B5-INV", "SMILE-G3-T10-INV", "SMILE-S6-HV-INV"] # List of known inverters -KNOWN_CHARGERS = ["SMILE-EVCT11"] +KNOWN_CHARGERS = ["SMILE-EVCT11", "SMILE-EVCS7"] # Set blacklist for certain inverters from certain sensors INVERTER_SETTING_BLACKLIST = [ "VT1000"] # Blacklist sensors for setting discharge/charge amount and sending discharge and charge amount From fa61a1399674fe1e3b7f3c7e376f9300dfcb9ef4 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 1 Mar 2026 23:45:44 +1030 Subject: [PATCH 03/13] Add currency diagnostic sensor, and only pass EV charger sensor if not None --- README.md | 15 +++ custom_components/alphaess/coordinator.py | 36 +++++- custom_components/alphaess/enums.py | 9 ++ custom_components/alphaess/sensor.py | 38 ++++-- custom_components/alphaess/sensorlist.py | 144 ++++++++++++++++++++++ 5 files changed, 229 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 36a472a..0a6d9cb 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,21 @@ The integration exposes EV charger controls (start/stop and current setting) whe These track whether a start/stop command is currently valid based on the latest EV charger status. +### Currency and daily history sensors + +- Monetary sensors now use ISO 4217 currency codes when provided by the API, and fall back to Home Assistant's configured currency when not available. +- Diagnostic currency sensors are available for troubleshooting: + - `Currency Code` +- Daily history energy breakdown sensors are exposed: + - `Daily PV Generation` + - `Daily Grid Consumption` + - `Daily Feed-in` + - `Daily Grid Charge` + - `Daily Battery Charge` + - `Daily Battery Discharge` + - `Daily EV Charging Energy` + - `Daily Energy Date` + If you want to adjust the restrictions yourself, you are able to by modifying the `ALPHA_POST_REQUEST_RESTRICTION` varible in const.py to the amount of seconds allowed per call ## Local Inverter Support diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 523f0fe..6d5f4be 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -24,6 +24,18 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) +def _normalize_currency_code(value: Any) -> str | None: + """Return ISO 4217 currency code if valid, otherwise None.""" + if not isinstance(value, str): + return None + + normalized = value.strip() + if len(normalized) == 3 and normalized.isalpha(): + return normalized.upper() + + return None + + class DataProcessor: """Helper class for data processing utilities.""" @@ -145,6 +157,9 @@ async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[s async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: """Parse summary statistics.""" + currency = await self.dp.safe_get(sum_data, "moneyType") + currency_code = _normalize_currency_code(currency) + data = { AlphaESSNames.TotalLoad: await self.dp.safe_get(sum_data, "eload"), AlphaESSNames.Income: await self.dp.safe_get(sum_data, "totalIncome"), @@ -153,7 +168,8 @@ async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: AlphaESSNames.carbonReduction: await self.dp.safe_get(sum_data, "carbonNum"), AlphaESSNames.TodayGeneration: await self.dp.safe_get(sum_data, "epvtoday"), AlphaESSNames.TodayIncome: await self.dp.safe_get(sum_data, "todayIncome"), - "Currency": await self.dp.safe_get(sum_data, "moneyType"), + AlphaESSNames.CurrencyCode: currency_code, + "Currency": currency_code, } # Handle self consumption and sufficiency correctly @@ -171,17 +187,29 @@ async def parse_energy_data(self, energy_data: Dict) -> Dict[str, Any]: feedin = await self.dp.safe_get(energy_data, "eOutput") gridcharge = await self.dp.safe_get(energy_data, "eGridCharge") charge = await self.dp.safe_get(energy_data, "eCharge") + grid_consumption = await self.dp.safe_get(energy_data, "eInput") + discharge = await self.dp.safe_get(energy_data, "eDischarge") + ev_energy = await self.dp.safe_get(energy_data, "eChargingPile") + energy_date = await self.dp.safe_get(energy_data, "theDate") return { AlphaESSNames.SolarProduction: pv, AlphaESSNames.SolarToLoad: await self.dp.safe_calculate(pv, feedin), AlphaESSNames.SolarToGrid: feedin, AlphaESSNames.SolarToBattery: await self.dp.safe_calculate(charge, gridcharge), - AlphaESSNames.GridToLoad: await self.dp.safe_get(energy_data, "eInput"), + AlphaESSNames.GridToLoad: grid_consumption, AlphaESSNames.GridToBattery: gridcharge, AlphaESSNames.Charge: charge, - AlphaESSNames.Discharge: await self.dp.safe_get(energy_data, "eDischarge"), - AlphaESSNames.EVCharger: await self.dp.safe_get(energy_data, "eChargingPile"), + AlphaESSNames.Discharge: discharge, + AlphaESSNames.EVCharger: ev_energy, + AlphaESSNames.DailyPvGeneration: pv, + AlphaESSNames.DailyGridConsumption: grid_consumption, + AlphaESSNames.DailyFeedIn: feedin, + AlphaESSNames.DailyGridCharge: gridcharge, + AlphaESSNames.DailyBatteryCharge: charge, + AlphaESSNames.DailyBatteryDischarge: discharge, + AlphaESSNames.DailyEvChargingEnergy: ev_energy, + AlphaESSNames.DailyEnergyDate: energy_date, } async def parse_power_data(self, power_data: Dict, one_day_power: Optional[list]) -> Dict[str, Any]: diff --git a/custom_components/alphaess/enums.py b/custom_components/alphaess/enums.py index 45071bf..123cee8 100644 --- a/custom_components/alphaess/enums.py +++ b/custom_components/alphaess/enums.py @@ -85,6 +85,15 @@ class AlphaESSNames(str, Enum): DischargeTimeControlEnabled = "Discharge Time Control Enabled" TodayGeneration = "Today's Generation" TodayIncome = "Today's Income" + DailyPvGeneration = "Daily PV Generation" + DailyGridConsumption = "Daily Grid Consumption" + DailyFeedIn = "Daily Feed-in" + DailyGridCharge = "Daily Grid Charge" + DailyBatteryCharge = "Daily Battery Charge" + DailyBatteryDischarge = "Daily Battery Discharge" + DailyEvChargingEnergy = "Daily EV Charging Energy" + DailyEnergyDate = "Daily Energy Date" + CurrencyCode = "Currency Code" treePlanted = "Trees Planted" carbonReduction = "Co2 Reduction" softwareVersion = "Software Version" diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 2bb8e79..f22728e 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -21,6 +21,23 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) + +def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | None: + """ + Normalize currency values to ISO 4217 codes only. + """ + if value is None: + return fallback + + normalized = value.strip() + if not normalized: + return fallback + + if len(normalized) == 3 and normalized.isalpha(): + return normalized.upper() + + return fallback + EV_RELATED_KEYS = { AlphaESSNames.evchargersn, AlphaESSNames.evchargermodel, @@ -137,9 +154,10 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - currency = data.get("Currency") - if currency is None: - currency = hass.config.currency + currency = _normalize_currency_unit( + data.get(AlphaESSNames.CurrencyCode) or data.get("Currency"), + hass.config.currency, + ) _LOGGER.info(f"New Inverter: Serial: {serial}, Model: {model}") @@ -193,9 +211,10 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - currency = data.get("Currency") - if currency is None: - currency = hass.config.currency + currency = _normalize_currency_unit( + data.get(AlphaESSNames.CurrencyCode) or data.get("Currency"), + hass.config.currency, + ) ev_model = data.get("EV Charger Model") _LOGGER.info(f"New EV Charger: Serial: {ev_charger}, Model: {ev_model}") @@ -225,9 +244,10 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger or ev_charger in ev_subentry_serials: continue - currency = data.get("Currency") - if currency is None: - currency = hass.config.currency + currency = _normalize_currency_unit( + data.get(AlphaESSNames.CurrencyCode) or data.get("Currency"), + hass.config.currency, + ) _add_ev_entities( coordinator, entry, serial, data, currency, diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 275ef2f..2ae43aa 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -416,6 +416,78 @@ device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyPvGeneration, + name="Daily PV Generation", + icon="mdi:solar-power-variant", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyGridConsumption, + name="Daily Grid Consumption", + icon="mdi:transmission-tower-import", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyFeedIn, + name="Daily Feed-in", + icon="mdi:transmission-tower-export", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyGridCharge, + name="Daily Grid Charge", + icon="mdi:battery-arrow-down", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyBatteryCharge, + name="Daily Battery Charge", + icon="mdi:battery-plus", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyBatteryDischarge, + name="Daily Battery Discharge", + icon="mdi:battery-minus", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyEvChargingEnergy, + name="Daily EV Charging Energy", + icon="mdi:car-electric", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyEnergyDate, + name="Daily Energy Date", + icon="mdi:calendar", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.CurrencyCode, + name="Currency Code", + icon="mdi:currency-usd", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), ] LIMITED_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ @@ -735,6 +807,78 @@ device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyPvGeneration, + name="Daily PV Generation", + icon="mdi:solar-power-variant", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyGridConsumption, + name="Daily Grid Consumption", + icon="mdi:transmission-tower-import", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyFeedIn, + name="Daily Feed-in", + icon="mdi:transmission-tower-export", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyGridCharge, + name="Daily Grid Charge", + icon="mdi:battery-arrow-down", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyBatteryCharge, + name="Daily Battery Charge", + icon="mdi:battery-plus", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyBatteryDischarge, + name="Daily Battery Discharge", + icon="mdi:battery-minus", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyEvChargingEnergy, + name="Daily EV Charging Energy", + icon="mdi:car-electric", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyEnergyDate, + name="Daily Energy Date", + icon="mdi:calendar", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.CurrencyCode, + name="Currency Code", + icon="mdi:currency-usd", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), ] SUPPORT_DISCHARGE_AND_CHARGE_BUTTON_DESCRIPTIONS: List[AlphaESSButtonDescription] = [ From 295c64a7581a07ccc6c1d3e09a157d6a2c579bd7 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 2 Mar 2026 00:09:18 +1030 Subject: [PATCH 04/13] Fix current diagnostic not working, and also make pev unavalible if no ev charger present --- custom_components/alphaess/coordinator.py | 19 ++++-------------- custom_components/alphaess/sensor.py | 24 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 6d5f4be..028f96e 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -24,18 +24,6 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) -def _normalize_currency_code(value: Any) -> str | None: - """Return ISO 4217 currency code if valid, otherwise None.""" - if not isinstance(value, str): - return None - - normalized = value.strip() - if len(normalized) == 3 and normalized.isalpha(): - return normalized.upper() - - return None - - class DataProcessor: """Helper class for data processing utilities.""" @@ -158,7 +146,6 @@ async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[s async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: """Parse summary statistics.""" currency = await self.dp.safe_get(sum_data, "moneyType") - currency_code = _normalize_currency_code(currency) data = { AlphaESSNames.TotalLoad: await self.dp.safe_get(sum_data, "eload"), @@ -168,10 +155,12 @@ async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: AlphaESSNames.carbonReduction: await self.dp.safe_get(sum_data, "carbonNum"), AlphaESSNames.TodayGeneration: await self.dp.safe_get(sum_data, "epvtoday"), AlphaESSNames.TodayIncome: await self.dp.safe_get(sum_data, "todayIncome"), - AlphaESSNames.CurrencyCode: currency_code, - "Currency": currency_code, } + if currency is not None: + data[AlphaESSNames.CurrencyCode] = currency + data["Currency"] = currency + # Handle self consumption and sufficiency correctly self_consumption = await self.dp.safe_get(sum_data, "eselfConsumption") self_sufficiency = await self.dp.safe_get(sum_data, "eselfSufficiency") diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index f22728e..2bd3725 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -38,6 +38,7 @@ def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | N return fallback + EV_RELATED_KEYS = { AlphaESSNames.evchargersn, AlphaESSNames.evchargermodel, @@ -168,6 +169,16 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if model in LIMITED_INVERTER_SENSOR_LIST: for description in limited_key_supported_states: + if ( + description == AlphaESSNames.pev + and data.get(AlphaESSNames.ElectricVehiclePowerOne) is None + ): + continue + if ( + description in EV_CONNECTOR_POWER_KEYS + and data.get(description) is None + ): + continue inverter_entities.append( AlphaESSSensor( coordinator, entry, serial, @@ -177,6 +188,16 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: ) else: for description in full_key_supported_states: + if ( + description == AlphaESSNames.pev + and data.get(AlphaESSNames.ElectricVehiclePowerOne) is None + ): + continue + if ( + description in EV_CONNECTOR_POWER_KEYS + and data.get(description) is None + ): + continue inverter_entities.append( AlphaESSSensor( coordinator, entry, serial, @@ -308,6 +329,9 @@ def available(self) -> bool: if self._key in EV_RELATED_KEYS and serial_data.get(AlphaESSNames.evchargersn) is None: return False + if self._key in (AlphaESSNames.pev, AlphaESSNames.ElectricVehiclePowerOne): + return serial_data.get(AlphaESSNames.ElectricVehiclePowerOne) is not None + if self._key in EV_CONNECTOR_POWER_KEYS: return serial_data.get(self._key) is not None From 5a8ae6d1d724effd74102fd19183b8088ca4cb5c Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 2 Mar 2026 00:20:27 +1030 Subject: [PATCH 05/13] Add One-time startup cleanup migration for ev entties that dont exist anymore --- custom_components/alphaess/__init__.py | 74 +++++++++++++++++++ custom_components/alphaess/config_flow.py | 14 ++++ custom_components/alphaess/const.py | 4 + custom_components/alphaess/coordinator.py | 8 +- custom_components/alphaess/strings.json | 3 +- .../alphaess/translations/en.json | 3 +- 6 files changed, 103 insertions(+), 3 deletions(-) diff --git a/custom_components/alphaess/__init__.py b/custom_components/alphaess/__init__.py index 2b78ee1..44cd864 100644 --- a/custom_components/alphaess/__init__.py +++ b/custom_components/alphaess/__init__.py @@ -4,6 +4,7 @@ import asyncio import ipaddress import logging +from datetime import timedelta import voluptuous as vol @@ -17,10 +18,14 @@ import homeassistant.helpers.config_validation as cv from .const import ( + CONF_SCAN_INTERVAL_SECONDS, CONF_DISABLE_NOTIFICATIONS, CONF_EV_CHARGER_MODEL, CONF_INVERTER_MODEL, CONF_IP_ADDRESS, + DEFAULT_SCAN_INTERVAL_SECONDS, + MAX_SCAN_INTERVAL_SECONDS, + MIN_SCAN_INTERVAL_SECONDS, CONF_PARENT_INVERTER, CONF_SERIAL_NUMBER, DOMAIN, @@ -29,6 +34,7 @@ SUBENTRY_TYPE_INVERTER, ) from .coordinator import AlphaESSDataUpdateCoordinator +from .enums import AlphaESSNames _LOGGER = logging.getLogger(__name__) @@ -140,6 +146,53 @@ def _migrate_entity_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: ent_reg.async_update_entity(entity_entry.entity_id, new_entity_id=desired_id) +def _cleanup_stale_ev_entities( + hass: HomeAssistant, + entry: ConfigEntry, + coordinator: AlphaESSDataUpdateCoordinator, +) -> None: + """Remove stale EV entities that are no longer supported for each inverter. + + This is a one-time migration helper to remove old EV entities from the + registry when the latest coordinator data indicates they should not exist. + """ + ent_reg = er.async_get(hass) + entities = er.async_entries_for_config_entry(ent_reg, entry.entry_id) + prefix = f"{entry.entry_id}_" + + connector_name_to_key = { + AlphaESSNames.ElectricVehiclePowerOne.value: AlphaESSNames.ElectricVehiclePowerOne, + AlphaESSNames.ElectricVehiclePowerTwo.value: AlphaESSNames.ElectricVehiclePowerTwo, + AlphaESSNames.ElectricVehiclePowerThree.value: AlphaESSNames.ElectricVehiclePowerThree, + AlphaESSNames.ElectricVehiclePowerFour.value: AlphaESSNames.ElectricVehiclePowerFour, + } + + for entity_entry in entities: + uid = entity_entry.unique_id + if not uid or not uid.startswith(prefix) or " - " not in uid: + continue + + remainder = uid[len(prefix):] + serial, entity_name = remainder.split(" - ", 1) + serial_data = coordinator.data.get(serial) + if not serial_data: + continue + + ev_present = serial_data.get(AlphaESSNames.evchargersn) is not None + connector_one_present = serial_data.get(AlphaESSNames.ElectricVehiclePowerOne) is not None + + should_remove = False + if entity_name == AlphaESSNames.pev.value: + should_remove = (not ev_present) or (not connector_one_present) + elif entity_name in connector_name_to_key: + connector_key = connector_name_to_key[entity_name] + should_remove = (not ev_present) or (serial_data.get(connector_key) is None) + + if should_remove: + _LOGGER.info("Removing stale EV entity %s", entity_entry.entity_id) + ent_reg.async_remove(entity_entry.entity_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Alpha ESS from a config entry.""" @@ -206,6 +259,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: inverter_models = _build_inverter_model_list(entry) + scan_interval_seconds = entry.options.get( + CONF_SCAN_INTERVAL_SECONDS, + DEFAULT_SCAN_INTERVAL_SECONDS, + ) + try: + scan_interval_seconds = int(scan_interval_seconds) + except (TypeError, ValueError): + scan_interval_seconds = DEFAULT_SCAN_INTERVAL_SECONDS + + scan_interval_seconds = max( + MIN_SCAN_INTERVAL_SECONDS, + min(MAX_SCAN_INTERVAL_SECONDS, scan_interval_seconds), + ) + await asyncio.sleep(1) _coordinator = AlphaESSDataUpdateCoordinator( @@ -214,6 +281,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ip_address_map=ip_address_map, inverter_models=inverter_models, entry=entry, + scan_interval=timedelta(seconds=scan_interval_seconds), ) await _coordinator.async_config_entry_first_refresh() @@ -242,6 +310,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_add_subentry(entry, ev_subentry) existing_ev_serials.add(ev_sn) + # One-time cleanup: remove stale EV entities no longer supported by data. + if not entry.options.get("_ev_entity_cleanup_done", False): + _cleanup_stale_ev_entities(hass, entry, _coordinator) + new_options = {**entry.options, "_ev_entity_cleanup_done": True} + hass.config_entries.async_update_entry(entry, options=new_options) + # One-time cleanup: remove old device associations from pre-subentry era. # Old devices were registered with (config_entry_id, None) - no subentry. # Remove them so platforms recreate devices with proper subentry associations. diff --git a/custom_components/alphaess/config_flow.py b/custom_components/alphaess/config_flow.py index 86a649d..a129205 100644 --- a/custom_components/alphaess/config_flow.py +++ b/custom_components/alphaess/config_flow.py @@ -21,11 +21,15 @@ from homeassistant.exceptions import HomeAssistantError from .const import ( + CONF_SCAN_INTERVAL_SECONDS, CONF_DISABLE_NOTIFICATIONS, CONF_INVERTER_MODEL, CONF_IP_ADDRESS, CONF_SERIAL_NUMBER, + DEFAULT_SCAN_INTERVAL_SECONDS, DOMAIN, + MAX_SCAN_INTERVAL_SECONDS, + MIN_SCAN_INTERVAL_SECONDS, SUBENTRY_TYPE_INVERTER, ) @@ -181,6 +185,16 @@ async def async_step_init( self._config_entry.data.get("Verify SSL Certificate", True), ), ): bool, + vol.Optional( + CONF_SCAN_INTERVAL_SECONDS, + default=self._config_entry.options.get( + CONF_SCAN_INTERVAL_SECONDS, + DEFAULT_SCAN_INTERVAL_SECONDS, + ), + ): vol.All( + vol.Coerce(int), + vol.Range(min=MIN_SCAN_INTERVAL_SECONDS, max=MAX_SCAN_INTERVAL_SECONDS), + ), } return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index 9cda368..b70f350 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -14,6 +14,9 @@ Platform.TIME, ] SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_SCAN_INTERVAL_SECONDS = int(SCAN_INTERVAL.total_seconds()) +MIN_SCAN_INTERVAL_SECONDS = 10 +MAX_SCAN_INTERVAL_SECONDS = 3600 ALPHA_POST_REQUEST_RESTRICTION = timedelta(seconds=30) # Subentry types @@ -27,6 +30,7 @@ CONF_INVERTER_MODEL = "inverter_model" CONF_EV_CHARGER_MODEL = "ev_charger_model" CONF_DISABLE_NOTIFICATIONS = "disable_notifications" +CONF_SCAN_INTERVAL_SECONDS = "scan_interval_seconds" KNOWN_INVERTERS = ["Storion-S5", "SMILE5-INV", "VT1000", "SMILE-T10-HV-INV", "SMILE-G3-B5-INV", "SMILE-G3-T10-INV", "SMILE-S6-HV-INV"] # List of known inverters diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 028f96e..151ae8f 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -325,9 +325,15 @@ def __init__( ip_address_map: dict[str, str | None] | None = None, inverter_models: list[str] | None = None, entry: ConfigEntry | None = None, + scan_interval: timedelta | None = None, ) -> None: """Initialize coordinator.""" - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=scan_interval or SCAN_INTERVAL, + ) self.api = client self.hass = hass self.data: dict[str, dict[str, float]] = {} diff --git a/custom_components/alphaess/strings.json b/custom_components/alphaess/strings.json index cc3ed92..c49b9e8 100644 --- a/custom_components/alphaess/strings.json +++ b/custom_components/alphaess/strings.json @@ -77,7 +77,8 @@ "step": { "init": { "data": { - "Verify SSL Certificate": "Verify SSL Certificate" + "Verify SSL Certificate": "Verify SSL Certificate", + "scan_interval_seconds": "Scan interval (seconds)" } } } diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index 6090425..393a1f3 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -79,7 +79,8 @@ "step": { "init": { "data": { - "Verify SSL Certificate": "Verify SSL Certificate" + "Verify SSL Certificate": "Verify SSL Certificate", + "scan_interval_seconds": "Scan interval (seconds)" } } } From e55aa03aee4b0860dea65a2ee580519ad185e303 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 2 Mar 2026 00:29:42 +1030 Subject: [PATCH 06/13] remove fallback --- custom_components/alphaess/sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 2bd3725..5a7a18a 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -24,7 +24,10 @@ def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | None: """ - Normalize currency values to ISO 4217 codes only. + Normalize currency for monetary units while preserving API-provided symbols. + + If AlphaESS provides a currency symbol (e.g. €), keep it to avoid + switching units to HA's configured currency. """ if value is None: return fallback @@ -36,7 +39,7 @@ def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | N if len(normalized) == 3 and normalized.isalpha(): return normalized.upper() - return fallback + return normalized EV_RELATED_KEYS = { From 96dea061550e3fdb75f2a71fb0b2f0fd94f1aa99 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 18:06:23 +1030 Subject: [PATCH 07/13] Preserve internal flags in config flow during option saves --- custom_components/alphaess/config_flow.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/alphaess/config_flow.py b/custom_components/alphaess/config_flow.py index a129205..de39849 100644 --- a/custom_components/alphaess/config_flow.py +++ b/custom_components/alphaess/config_flow.py @@ -175,7 +175,14 @@ async def async_step_init( self, user_input: dict[str, Any] | None = None ): if user_input is not None: - return self.async_create_entry(title="", data=user_input) + # Preserve internal flags (keys starting with '_') across option saves + merged = { + k: v + for k, v in self._config_entry.options.items() + if k.startswith("_") + } + merged.update(user_input) + return self.async_create_entry(title="", data=merged) schema = { vol.Optional( From 56be07a7ad8fe9428c4110c9e6f846427b94f79c Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 18:07:07 +1030 Subject: [PATCH 08/13] Bump version to 0.8.4 --- custom_components/alphaess/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/alphaess/manifest.json b/custom_components/alphaess/manifest.json index 1b23b54..7508272 100644 --- a/custom_components/alphaess/manifest.json +++ b/custom_components/alphaess/manifest.json @@ -15,7 +15,7 @@ "requirements": [ "alphaessopenapi==0.0.17" ], - "version": "0.8.3" + "version": "0.8.4" } From d5f5c4443474ff07647ea55a0d71ca27b8895f4a Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 18:37:03 +1030 Subject: [PATCH 09/13] Extract and centralize `build_inverter_device_info` and `build_ev_charger_device_info` into a new `device.py` module. --- custom_components/alphaess/binary_sensor.py | 6 +-- custom_components/alphaess/button.py | 9 ++-- custom_components/alphaess/device.py | 53 +++++++++++++++++++++ custom_components/alphaess/number.py | 9 ++-- custom_components/alphaess/sensor.py | 51 ++------------------ custom_components/alphaess/switch.py | 4 +- custom_components/alphaess/time.py | 4 +- 7 files changed, 71 insertions(+), 65 deletions(-) create mode 100644 custom_components/alphaess/device.py diff --git a/custom_components/alphaess/binary_sensor.py b/custom_components/alphaess/binary_sensor.py index 12961f6..4c94ad7 100644 --- a/custom_components/alphaess/binary_sensor.py +++ b/custom_components/alphaess/binary_sensor.py @@ -14,7 +14,7 @@ ) from .coordinator import AlphaESSDataUpdateCoordinator from .sensorlist import EV_CHARGER_BINARY_SENSORS -from .sensor import _build_ev_charger_device_info +from .device import build_ev_charger_device_info _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if ev_charger in ev_subentry_serials: continue - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( @@ -73,7 +73,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index bf98b54..f8b1cd9 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -2,7 +2,6 @@ from typing import List import logging from homeassistant.components.button import ButtonEntity, ButtonDeviceClass -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, ALPHA_POST_REQUEST_RESTRICTION, INVERTER_SETTING_BLACKLIST, CONF_SERIAL_NUMBER, \ @@ -10,7 +9,7 @@ from .coordinator import AlphaESSDataUpdateCoordinator from .sensorlist import SUPPORT_DISCHARGE_AND_CHARGE_BUTTON_DESCRIPTIONS, EV_DISCHARGE_AND_CHARGE_BUTTONS from .enums import AlphaESSNames -from .sensor import _build_inverter_device_info, _build_ev_charger_device_info +from .device import build_inverter_device_info, build_ev_charger_device_info _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -47,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = _build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) inverter_buttons: List[ButtonEntity] = [] @@ -70,7 +69,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) for description in ev_charging_supported_states: inverter_buttons.append( AlphaESSBatteryButton( @@ -99,7 +98,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_buttons: List[ButtonEntity] = [] for description in ev_charging_supported_states: ev_buttons.append( diff --git a/custom_components/alphaess/device.py b/custom_components/alphaess/device.py new file mode 100644 index 0000000..fae88fe --- /dev/null +++ b/custom_components/alphaess/device.py @@ -0,0 +1,53 @@ +"""Shared device info builders for AlphaESS integration.""" + +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo + +from .const import DOMAIN +from .coordinator import AlphaESSDataUpdateCoordinator + + +def build_inverter_device_info( + coordinator: AlphaESSDataUpdateCoordinator, + serial: str, + data: dict, +) -> DeviceInfo: + """Build DeviceInfo for an inverter.""" + serial_upper = serial.upper() + + kwargs = { + "entry_type": DeviceEntryType.SERVICE, + "identifiers": {(DOMAIN, serial_upper)}, + "manufacturer": "AlphaESS", + "model": data.get("Model"), + "model_id": serial, + "name": f"Alpha ESS Energy Statistics : {serial_upper}", + } + + if "Local IP" in data and data.get("Local IP") != "0" and data.get("Device Status") is not None: + kwargs["serial_number"] = data.get("Device Serial Number") + kwargs["sw_version"] = data.get("Software Version") + kwargs["hw_version"] = data.get("Hardware Version") + kwargs["configuration_url"] = f"http://{data['Local IP']}" + + return DeviceInfo(**kwargs) + + +def build_ev_charger_device_info( + coordinator: AlphaESSDataUpdateCoordinator, + data: dict, +) -> DeviceInfo: + """Build DeviceInfo for an EV charger.""" + ev_sn = data.get("EV Charger S/N") + + kwargs = { + "entry_type": DeviceEntryType.SERVICE, + "identifiers": {(DOMAIN, ev_sn)}, + "manufacturer": "AlphaESS", + "model": data.get("EV Charger Model"), + "model_id": ev_sn, + "name": f"Alpha ESS Charger : {ev_sn}", + } + + return DeviceInfo(**kwargs) + diff --git a/custom_components/alphaess/number.py b/custom_components/alphaess/number.py index 1ad9ad9..f7d8b51 100644 --- a/custom_components/alphaess/number.py +++ b/custom_components/alphaess/number.py @@ -1,6 +1,5 @@ from typing import List from homeassistant.components.number import NumberEntity -from homeassistant.helpers.device_registry import DeviceInfo, DeviceEntryType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.components.number import RestoreNumber import logging @@ -12,7 +11,7 @@ from .coordinator import AlphaESSDataUpdateCoordinator from .enums import AlphaESSNames from .sensorlist import DISCHARGE_AND_CHARGE_NUMBERS, EV_CHARGER_NUMBERS -from .sensor import _build_inverter_device_info, _build_ev_charger_device_info +from .device import build_inverter_device_info, build_ev_charger_device_info _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -36,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = _build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) number_entities: List[NumberEntity] = [] @@ -58,7 +57,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) for description in ev_number_supported_states: number_entities.append( AlphaEVNumber( @@ -85,7 +84,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_entities: List[NumberEntity] = [] for description in ev_number_supported_states: ev_entities.append( diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 5a7a18a..5a901b7 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -11,13 +11,12 @@ from .enums import AlphaESSNames from .sensorlist import FULL_SENSOR_DESCRIPTIONS, LIMITED_SENSOR_DESCRIPTIONS, EV_CHARGING_DETAILS, LOCAL_IP_SYSTEM_SENSORS -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LIMITED_INVERTER_SENSOR_LIST, EV_CHARGER_STATE_KEYS, TCP_STATUS_KEYS, ETHERNET_STATUS_KEYS, \ FOUR_G_STATUS_KEYS, WIFI_STATUS_KEYS, CONF_SERIAL_NUMBER, SUBENTRY_TYPE_INVERTER, SUBENTRY_TYPE_EV_CHARGER, \ CONF_PARENT_INVERTER from .coordinator import AlphaESSDataUpdateCoordinator +from .device import build_inverter_device_info, build_ev_charger_device_info _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -62,56 +61,12 @@ def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | N } -def _build_inverter_device_info( - coordinator: AlphaESSDataUpdateCoordinator, - serial: str, - data: dict, -) -> DeviceInfo: - """Build DeviceInfo for an inverter.""" - serial_upper = serial.upper() - - kwargs = { - "entry_type": DeviceEntryType.SERVICE, - "identifiers": {(DOMAIN, serial_upper)}, - "manufacturer": "AlphaESS", - "model": data.get("Model"), - "model_id": serial, - "name": f"Alpha ESS Energy Statistics : {serial_upper}", - } - - if "Local IP" in data and data.get("Local IP") != "0" and data.get("Device Status") is not None: - kwargs["serial_number"] = data.get("Device Serial Number") - kwargs["sw_version"] = data.get("Software Version") - kwargs["hw_version"] = data.get("Hardware Version") - kwargs["configuration_url"] = f"http://{data['Local IP']}" - - return DeviceInfo(**kwargs) - - -def _build_ev_charger_device_info( - coordinator: AlphaESSDataUpdateCoordinator, - data: dict, -) -> DeviceInfo: - """Build DeviceInfo for an EV charger.""" - ev_sn = data.get("EV Charger S/N") - - kwargs = { - "entry_type": DeviceEntryType.SERVICE, - "identifiers": {(DOMAIN, ev_sn)}, - "manufacturer": "AlphaESS", - "model": data.get("EV Charger Model"), - "model_id": ev_sn, - "name": f"Alpha ESS Charger : {ev_sn}", - } - - return DeviceInfo(**kwargs) - def _add_ev_entities(coordinator, entry, serial, data, currency, ev_charging_supported_states, subentry_id, async_add_entities): """Create and register EV charger sensor entities.""" ev_charger = data.get("EV Charger S/N") ev_model = data.get("EV Charger Model") - ev_device_info = _build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(coordinator, data) _LOGGER.info(f"New EV Charger: Serial: {ev_charger}, Model: {ev_model}") ev_entities: List[AlphaESSSensor] = [] @@ -166,7 +121,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: _LOGGER.info(f"New Inverter: Serial: {serial}, Model: {model}") has_local_ip_data = 'Local IP' in data - inverter_device_info = _build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) inverter_entities: List[AlphaESSSensor] = [] diff --git a/custom_components/alphaess/switch.py b/custom_components/alphaess/switch.py index 8467ba5..788cc39 100644 --- a/custom_components/alphaess/switch.py +++ b/custom_components/alphaess/switch.py @@ -11,7 +11,7 @@ from .coordinator import AlphaESSDataUpdateCoordinator from .enums import AlphaESSNames from .sensorlist import CHARGE_DISCHARGE_SWITCHES -from .sensor import _build_inverter_device_info +from .device import build_inverter_device_info _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -34,7 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = _build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) switch_entities: List[SwitchEntity] = [] diff --git a/custom_components/alphaess/time.py b/custom_components/alphaess/time.py index 9bfa826..80dbdcd 100644 --- a/custom_components/alphaess/time.py +++ b/custom_components/alphaess/time.py @@ -10,7 +10,7 @@ from .coordinator import AlphaESSDataUpdateCoordinator from .enums import AlphaESSNames from .sensorlist import CHARGE_DISCHARGE_TIMES -from .sensor import _build_inverter_device_info +from .device import build_inverter_device_info _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = _build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) time_entities: List[TimeEntity] = [] From cbef404ab89f13c15391bd60d4b76972b98a2375 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 18:55:06 +1030 Subject: [PATCH 10/13] Simplify `build_inverter_device_info` and `build_ev_charger_device_info` by removing unused coordinator parameter, deduplicate common sensor definitions. --- custom_components/alphaess/binary_sensor.py | 15 +- custom_components/alphaess/button.py | 6 +- custom_components/alphaess/coordinator.py | 5 +- custom_components/alphaess/device.py | 3 - custom_components/alphaess/number.py | 6 +- custom_components/alphaess/sensor.py | 4 +- custom_components/alphaess/sensorlist.py | 224 +++++++------------- custom_components/alphaess/switch.py | 2 +- custom_components/alphaess/time.py | 2 +- 9 files changed, 98 insertions(+), 169 deletions(-) diff --git a/custom_components/alphaess/binary_sensor.py b/custom_components/alphaess/binary_sensor.py index 4c94ad7..ce29009 100644 --- a/custom_components/alphaess/binary_sensor.py +++ b/custom_components/alphaess/binary_sensor.py @@ -27,6 +27,12 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: description.key: description for description in EV_CHARGER_BINARY_SENSORS } + ev_subentry_serials = { + sub.data.get(CONF_SERIAL_NUMBER) + for sub in entry.subentries.values() + if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER + } + for subentry in entry.subentries.values(): if subentry.subentry_type == SUBENTRY_TYPE_INVERTER: serial = subentry.data.get(CONF_SERIAL_NUMBER) @@ -38,15 +44,10 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_subentry_serials = { - sub.data.get(CONF_SERIAL_NUMBER) - for sub in entry.subentries.values() - if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER - } if ev_charger in ev_subentry_serials: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( @@ -73,7 +74,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index f8b1cd9..a971d06 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) inverter_buttons: List[ButtonEntity] = [] @@ -69,7 +69,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) for description in ev_charging_supported_states: inverter_buttons.append( AlphaESSBatteryButton( @@ -98,7 +98,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_buttons: List[ButtonEntity] = [] for description in ev_charging_supported_states: ev_buttons.append( diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 151ae8f..461d824 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -422,10 +422,9 @@ def can_control_ev(self, serial: str, direction: int) -> bool: return status in (3, 4, 5) return False - async def control_ev(self, serial: str, ev_serial: str, direction: str) -> None: + async def control_ev(self, serial: str, ev_serial: str, direction: int) -> None: """Control EV charger.""" - parsed_direction = int(direction) - if not self.can_control_ev(serial, parsed_direction): + if not self.can_control_ev(serial, direction): _LOGGER.warning( "Skipping EV control command for %s (%s), direction=%s due to incompatible state=%s", serial, diff --git a/custom_components/alphaess/device.py b/custom_components/alphaess/device.py index fae88fe..50151b9 100644 --- a/custom_components/alphaess/device.py +++ b/custom_components/alphaess/device.py @@ -4,11 +4,9 @@ from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN -from .coordinator import AlphaESSDataUpdateCoordinator def build_inverter_device_info( - coordinator: AlphaESSDataUpdateCoordinator, serial: str, data: dict, ) -> DeviceInfo: @@ -34,7 +32,6 @@ def build_inverter_device_info( def build_ev_charger_device_info( - coordinator: AlphaESSDataUpdateCoordinator, data: dict, ) -> DeviceInfo: """Build DeviceInfo for an EV charger.""" diff --git a/custom_components/alphaess/number.py b/custom_components/alphaess/number.py index f7d8b51..410b1e7 100644 --- a/custom_components/alphaess/number.py +++ b/custom_components/alphaess/number.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) number_entities: List[NumberEntity] = [] @@ -57,7 +57,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) for description in ev_number_supported_states: number_entities.append( AlphaEVNumber( @@ -84,7 +84,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_entities: List[NumberEntity] = [] for description in ev_number_supported_states: ev_entities.append( diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 5a901b7..49b797a 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -66,7 +66,7 @@ def _add_ev_entities(coordinator, entry, serial, data, currency, ev_charging_sup """Create and register EV charger sensor entities.""" ev_charger = data.get("EV Charger S/N") ev_model = data.get("EV Charger Model") - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) _LOGGER.info(f"New EV Charger: Serial: {ev_charger}, Model: {ev_model}") ev_entities: List[AlphaESSSensor] = [] @@ -121,7 +121,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: _LOGGER.info(f"New Inverter: Serial: {serial}, Model: {model}") has_local_ip_data = 'Local IP' in data - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) inverter_entities: List[AlphaESSSensor] = [] diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 2ae43aa..5366d2c 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -18,6 +18,82 @@ ) from .enums import AlphaESSNames +# Shared daily-energy and diagnostic sensors used in both FULL and LIMITED lists. +_COMMON_DAILY_SENSORS: List[AlphaESSSensorDescription] = [ + AlphaESSSensorDescription( + key=AlphaESSNames.DailyPvGeneration, + name="Daily PV Generation", + icon="mdi:solar-power-variant", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyGridConsumption, + name="Daily Grid Consumption", + icon="mdi:transmission-tower-import", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyFeedIn, + name="Daily Feed-in", + icon="mdi:transmission-tower-export", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyGridCharge, + name="Daily Grid Charge", + icon="mdi:battery-arrow-down", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyBatteryCharge, + name="Daily Battery Charge", + icon="mdi:battery-plus", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyBatteryDischarge, + name="Daily Battery Discharge", + icon="mdi:battery-minus", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyEvChargingEnergy, + name="Daily EV Charging Energy", + icon="mdi:car-electric", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DailyEnergyDate, + name="Daily Energy Date", + icon="mdi:calendar", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.CurrencyCode, + name="Currency Code", + icon="mdi:currency-usd", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + FULL_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( key=AlphaESSNames.SolarProduction, @@ -416,79 +492,7 @@ device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyPvGeneration, - name="Daily PV Generation", - icon="mdi:solar-power-variant", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyGridConsumption, - name="Daily Grid Consumption", - icon="mdi:transmission-tower-import", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyFeedIn, - name="Daily Feed-in", - icon="mdi:transmission-tower-export", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyGridCharge, - name="Daily Grid Charge", - icon="mdi:battery-arrow-down", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyBatteryCharge, - name="Daily Battery Charge", - icon="mdi:battery-plus", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyBatteryDischarge, - name="Daily Battery Discharge", - icon="mdi:battery-minus", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyEvChargingEnergy, - name="Daily EV Charging Energy", - icon="mdi:car-electric", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyEnergyDate, - name="Daily Energy Date", - icon="mdi:calendar", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.CurrencyCode, - name="Currency Code", - icon="mdi:currency-usd", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - ), -] +] + _COMMON_DAILY_SENSORS LIMITED_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( @@ -807,79 +811,7 @@ device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyPvGeneration, - name="Daily PV Generation", - icon="mdi:solar-power-variant", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyGridConsumption, - name="Daily Grid Consumption", - icon="mdi:transmission-tower-import", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyFeedIn, - name="Daily Feed-in", - icon="mdi:transmission-tower-export", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyGridCharge, - name="Daily Grid Charge", - icon="mdi:battery-arrow-down", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyBatteryCharge, - name="Daily Battery Charge", - icon="mdi:battery-plus", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyBatteryDischarge, - name="Daily Battery Discharge", - icon="mdi:battery-minus", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyEvChargingEnergy, - name="Daily EV Charging Energy", - icon="mdi:car-electric", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DailyEnergyDate, - name="Daily Energy Date", - icon="mdi:calendar", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.CurrencyCode, - name="Currency Code", - icon="mdi:currency-usd", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - ), -] +] + _COMMON_DAILY_SENSORS SUPPORT_DISCHARGE_AND_CHARGE_BUTTON_DESCRIPTIONS: List[AlphaESSButtonDescription] = [ AlphaESSButtonDescription( diff --git a/custom_components/alphaess/switch.py b/custom_components/alphaess/switch.py index 788cc39..eddbb2f 100644 --- a/custom_components/alphaess/switch.py +++ b/custom_components/alphaess/switch.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) switch_entities: List[SwitchEntity] = [] diff --git a/custom_components/alphaess/time.py b/custom_components/alphaess/time.py index 80dbdcd..d8ecfa9 100644 --- a/custom_components/alphaess/time.py +++ b/custom_components/alphaess/time.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) time_entities: List[TimeEntity] = [] From 911f08b1728aed679dc3f61da48b028fd1ac38dc Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 19:13:27 +1030 Subject: [PATCH 11/13] Refactor to pass `coordinator` to device info builders, adjust EV charger handling, and update sensor state classes to `TOTAL_INCREASING`. --- custom_components/alphaess/binary_sensor.py | 15 ++++--- custom_components/alphaess/button.py | 6 +-- custom_components/alphaess/coordinator.py | 5 ++- custom_components/alphaess/device.py | 3 ++ custom_components/alphaess/number.py | 6 +-- custom_components/alphaess/sensor.py | 43 +++++++++++++++++++-- custom_components/alphaess/sensorlist.py | 14 +++---- custom_components/alphaess/switch.py | 2 +- 8 files changed, 66 insertions(+), 28 deletions(-) diff --git a/custom_components/alphaess/binary_sensor.py b/custom_components/alphaess/binary_sensor.py index ce29009..4c94ad7 100644 --- a/custom_components/alphaess/binary_sensor.py +++ b/custom_components/alphaess/binary_sensor.py @@ -27,12 +27,6 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: description.key: description for description in EV_CHARGER_BINARY_SENSORS } - ev_subentry_serials = { - sub.data.get(CONF_SERIAL_NUMBER) - for sub in entry.subentries.values() - if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER - } - for subentry in entry.subentries.values(): if subentry.subentry_type == SUBENTRY_TYPE_INVERTER: serial = subentry.data.get(CONF_SERIAL_NUMBER) @@ -44,10 +38,15 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue + ev_subentry_serials = { + sub.data.get(CONF_SERIAL_NUMBER) + for sub in entry.subentries.values() + if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER + } if ev_charger in ev_subentry_serials: continue - ev_device_info = build_ev_charger_device_info(data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( @@ -74,7 +73,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index a971d06..f8b1cd9 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) inverter_buttons: List[ButtonEntity] = [] @@ -69,7 +69,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = build_ev_charger_device_info(data) + ev_device_info = build_ev_charger_device_info(coordinator, data) for description in ev_charging_supported_states: inverter_buttons.append( AlphaESSBatteryButton( @@ -98,7 +98,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_buttons: List[ButtonEntity] = [] for description in ev_charging_supported_states: ev_buttons.append( diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 461d824..151ae8f 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -422,9 +422,10 @@ def can_control_ev(self, serial: str, direction: int) -> bool: return status in (3, 4, 5) return False - async def control_ev(self, serial: str, ev_serial: str, direction: int) -> None: + async def control_ev(self, serial: str, ev_serial: str, direction: str) -> None: """Control EV charger.""" - if not self.can_control_ev(serial, direction): + parsed_direction = int(direction) + if not self.can_control_ev(serial, parsed_direction): _LOGGER.warning( "Skipping EV control command for %s (%s), direction=%s due to incompatible state=%s", serial, diff --git a/custom_components/alphaess/device.py b/custom_components/alphaess/device.py index 50151b9..fae88fe 100644 --- a/custom_components/alphaess/device.py +++ b/custom_components/alphaess/device.py @@ -4,9 +4,11 @@ from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN +from .coordinator import AlphaESSDataUpdateCoordinator def build_inverter_device_info( + coordinator: AlphaESSDataUpdateCoordinator, serial: str, data: dict, ) -> DeviceInfo: @@ -32,6 +34,7 @@ def build_inverter_device_info( def build_ev_charger_device_info( + coordinator: AlphaESSDataUpdateCoordinator, data: dict, ) -> DeviceInfo: """Build DeviceInfo for an EV charger.""" diff --git a/custom_components/alphaess/number.py b/custom_components/alphaess/number.py index 410b1e7..f7d8b51 100644 --- a/custom_components/alphaess/number.py +++ b/custom_components/alphaess/number.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) number_entities: List[NumberEntity] = [] @@ -57,7 +57,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = build_ev_charger_device_info(data) + ev_device_info = build_ev_charger_device_info(coordinator, data) for description in ev_number_supported_states: number_entities.append( AlphaEVNumber( @@ -84,7 +84,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(data) + ev_device_info = build_ev_charger_device_info(coordinator, data) ev_entities: List[NumberEntity] = [] for description in ev_number_supported_states: ev_entities.append( diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 49b797a..dc159f6 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -21,12 +21,40 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) +# Map common currency symbols to ISO 4217 codes. +# SensorDeviceClass.MONETARY expects an ISO 4217 currency code; raw symbols +# (e.g. '€') would produce invalid-unit warnings and broken statistics. +_SYMBOL_TO_ISO: dict[str, str] = { + "$": "USD", + "€": "EUR", + "£": "GBP", + "¥": "JPY", + "₩": "KRW", + "₹": "INR", + "₽": "RUB", + "₺": "TRY", + "R$": "BRL", + "₫": "VND", + "₴": "UAH", + "₱": "PHP", + "₦": "NGN", + "Fr": "CHF", + "kr": "SEK", + "zł": "PLN", + "A$": "AUD", + "C$": "CAD", + "NZ$": "NZD", + "R": "ZAR", +} + + def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | None: """ - Normalize currency for monetary units while preserving API-provided symbols. + Normalize currency for monetary units to an ISO 4217 code. - If AlphaESS provides a currency symbol (e.g. €), keep it to avoid - switching units to HA's configured currency. + SensorDeviceClass.MONETARY expects an ISO 4217 currency code. + If the API returns a known symbol we map it; otherwise we fall back + to the HA-configured currency. """ if value is None: return fallback @@ -35,10 +63,17 @@ def _normalize_currency_unit(value: str | None, fallback: str | None) -> str | N if not normalized: return fallback + # Already a 3-letter ISO code if len(normalized) == 3 and normalized.isalpha(): return normalized.upper() - return normalized + # Try to map a symbol to its ISO code + iso = _SYMBOL_TO_ISO.get(normalized) + if iso: + return iso + + # Unknown symbol — fall back to HA's configured currency + return fallback EV_RELATED_KEYS = { diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 5366d2c..8f64172 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -26,7 +26,7 @@ icon="mdi:solar-power-variant", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyGridConsumption, @@ -34,7 +34,7 @@ icon="mdi:transmission-tower-import", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyFeedIn, @@ -42,7 +42,7 @@ icon="mdi:transmission-tower-export", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyGridCharge, @@ -50,7 +50,7 @@ icon="mdi:battery-arrow-down", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyBatteryCharge, @@ -58,7 +58,7 @@ icon="mdi:battery-plus", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyBatteryDischarge, @@ -66,7 +66,7 @@ icon="mdi:battery-minus", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyEvChargingEnergy, @@ -74,7 +74,7 @@ icon="mdi:car-electric", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyEnergyDate, diff --git a/custom_components/alphaess/switch.py b/custom_components/alphaess/switch.py index eddbb2f..788cc39 100644 --- a/custom_components/alphaess/switch.py +++ b/custom_components/alphaess/switch.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(serial, data) + inverter_device_info = build_inverter_device_info(coordinator, serial, data) switch_entities: List[SwitchEntity] = [] From dfe2a4a1f45dd4464bc661a67fc7106ffc7c45b8 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 19:26:21 +1030 Subject: [PATCH 12/13] Remove unused coordinator param from device info builders and align all call sites --- custom_components/alphaess/binary_sensor.py | 4 ++-- custom_components/alphaess/button.py | 6 +++--- custom_components/alphaess/device.py | 3 --- custom_components/alphaess/number.py | 6 +++--- custom_components/alphaess/switch.py | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/custom_components/alphaess/binary_sensor.py b/custom_components/alphaess/binary_sensor.py index 4c94ad7..44d21bb 100644 --- a/custom_components/alphaess/binary_sensor.py +++ b/custom_components/alphaess/binary_sensor.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if ev_charger in ev_subentry_serials: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( @@ -73,7 +73,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_entities: List[BinarySensorEntity] = [] for description in ev_binary_supported_states.values(): ev_entities.append( diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index f8b1cd9..a971d06 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) inverter_buttons: List[ButtonEntity] = [] @@ -69,7 +69,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) for description in ev_charging_supported_states: inverter_buttons.append( AlphaESSBatteryButton( @@ -98,7 +98,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_buttons: List[ButtonEntity] = [] for description in ev_charging_supported_states: ev_buttons.append( diff --git a/custom_components/alphaess/device.py b/custom_components/alphaess/device.py index fae88fe..50151b9 100644 --- a/custom_components/alphaess/device.py +++ b/custom_components/alphaess/device.py @@ -4,11 +4,9 @@ from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN -from .coordinator import AlphaESSDataUpdateCoordinator def build_inverter_device_info( - coordinator: AlphaESSDataUpdateCoordinator, serial: str, data: dict, ) -> DeviceInfo: @@ -34,7 +32,6 @@ def build_inverter_device_info( def build_ev_charger_device_info( - coordinator: AlphaESSDataUpdateCoordinator, data: dict, ) -> DeviceInfo: """Build DeviceInfo for an EV charger.""" diff --git a/custom_components/alphaess/number.py b/custom_components/alphaess/number.py index f7d8b51..410b1e7 100644 --- a/custom_components/alphaess/number.py +++ b/custom_components/alphaess/number.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) number_entities: List[NumberEntity] = [] @@ -57,7 +57,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if sub.subentry_type == SUBENTRY_TYPE_EV_CHARGER } if ev_charger and ev_charger not in ev_subentry_serials: - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) for description in ev_number_supported_states: number_entities.append( AlphaEVNumber( @@ -84,7 +84,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: if not ev_charger: continue - ev_device_info = build_ev_charger_device_info(coordinator, data) + ev_device_info = build_ev_charger_device_info(data) ev_entities: List[NumberEntity] = [] for description in ev_number_supported_states: ev_entities.append( diff --git a/custom_components/alphaess/switch.py b/custom_components/alphaess/switch.py index 788cc39..eddbb2f 100644 --- a/custom_components/alphaess/switch.py +++ b/custom_components/alphaess/switch.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: data = coordinator.data[serial] model = data.get("Model") - inverter_device_info = build_inverter_device_info(coordinator, serial, data) + inverter_device_info = build_inverter_device_info(serial, data) switch_entities: List[SwitchEntity] = [] From a2f7a86d3729e9e6a3c03c833ff901f161eb6322 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Mar 2026 19:40:34 +1030 Subject: [PATCH 13/13] Gate EV entity cleanup on cloud_available; use TOTAL state_class for daily energy sensors --- custom_components/alphaess/__init__.py | 5 ++++- custom_components/alphaess/sensorlist.py | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/custom_components/alphaess/__init__.py b/custom_components/alphaess/__init__.py index 44cd864..8bc7420 100644 --- a/custom_components/alphaess/__init__.py +++ b/custom_components/alphaess/__init__.py @@ -311,7 +311,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: existing_ev_serials.add(ev_sn) # One-time cleanup: remove stale EV entities no longer supported by data. - if not entry.options.get("_ev_entity_cleanup_done", False): + # Only run when cloud data is available; in local-fallback mode EV keys are + # intentionally absent and we must not remove valid entities. + cloud_available = getattr(_coordinator, "cloud_available", True) + if cloud_available and not entry.options.get("_ev_entity_cleanup_done", False): _cleanup_stale_ev_entities(hass, entry, _coordinator) new_options = {**entry.options, "_ev_entity_cleanup_done": True} hass.config_entries.async_update_entry(entry, options=new_options) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 8f64172..5366d2c 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -26,7 +26,7 @@ icon="mdi:solar-power-variant", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyGridConsumption, @@ -34,7 +34,7 @@ icon="mdi:transmission-tower-import", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyFeedIn, @@ -42,7 +42,7 @@ icon="mdi:transmission-tower-export", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyGridCharge, @@ -50,7 +50,7 @@ icon="mdi:battery-arrow-down", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyBatteryCharge, @@ -58,7 +58,7 @@ icon="mdi:battery-plus", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyBatteryDischarge, @@ -66,7 +66,7 @@ icon="mdi:battery-minus", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyEvChargingEnergy, @@ -74,7 +74,7 @@ icon="mdi:car-electric", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), AlphaESSSensorDescription( key=AlphaESSNames.DailyEnergyDate,