Improve Charger Support/Use & more QOL#243
Conversation
…o ev charger present
There was a problem hiding this comment.
Pull request overview
This PR extends the AlphaESS Home Assistant integration with additional daily energy/history sensors, improved EV charger usability (diagnostics + command guardrails), and adds an options-flow setting to configure the coordinator scan interval.
Changes:
- Added daily energy breakdown sensors plus diagnostic sensors for energy date and currency code.
- Added EV charger “readiness” diagnostic binary sensors and guarded start/stop commands based on charger state.
- Added configurable scan interval (seconds) via the options flow and wired it into the coordinator update interval.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| custom_components/alphaess/translations/en.json | Adds options-flow label for scan interval. |
| custom_components/alphaess/strings.json | Adds options-flow label for scan interval (base strings). |
| custom_components/alphaess/sensorlist.py | Adds daily energy + currency diagnostic sensor descriptions; adds EV readiness binary sensor descriptions. |
| custom_components/alphaess/sensor.py | Currency unit normalization + EV entity availability/creation refinements; EV status parsing robustness. |
| custom_components/alphaess/manifest.json | Bumps integration version to 0.8.4. |
| custom_components/alphaess/enums.py | Adds enum keys for new daily/currency sensors and EV readiness sensors. |
| custom_components/alphaess/entity.py | Adds AlphaESSBinarySensorDescription. |
| custom_components/alphaess/coordinator.py | Adds currency mapping, daily energy fields, EV connector power filtering, scan interval injection, EV control guardrails. |
| custom_components/alphaess/const.py | Adds binary_sensor platform + scan interval bounds/constants; expands known chargers list. |
| custom_components/alphaess/config_flow.py | Adds scan interval option and preserves internal option flags across saves. |
| custom_components/alphaess/button.py | Adds EV start/stop guardrails with optional persistent notifications. |
| custom_components/alphaess/binary_sensor.py | New platform providing “Can Start Charging” / “Can Stop Charging” diagnostics. |
| custom_components/alphaess/init.py | Applies options-driven scan interval and adds one-time EV entity cleanup migration. |
| README.md | Documents EV charger controls/guardrails and the new currency + daily sensors. |
Comments suppressed due to low confidence (1)
custom_components/alphaess/coordinator.py:442
control_ev()convertsdirectionto an int for validation but still passes the original (possibly string) value through toremoteControlEvCharger(). This can lead to type/format mismatches and also raisesValueErrorifdirectionis ever non-numeric. Consider changing the method signature to accept anint, wrapping the conversion in atry/except(or removing it), and passing the validated/parsed integer through to the API call.
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} "
f"Direction: {direction} - Result: {result}"
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from .sensorlist import EV_CHARGER_BINARY_SENSORS | ||
| from .sensor import _build_ev_charger_device_info | ||
|
|
There was a problem hiding this comment.
binary_sensor.py imports _build_ev_charger_device_info from sensor.py, but the leading underscore indicates it is a private helper. Since it’s now used across modules, consider moving it to a shared utility module (or renaming it without the underscore) to make the intended public usage explicit and avoid future refactors accidentally breaking the binary sensor platform.
…rger_device_info` into a new `device.py` module.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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, | ||
| ), |
There was a problem hiding this comment.
The new daily-energy and currency sensor descriptions are duplicated verbatim in both FULL_SENSOR_DESCRIPTIONS and LIMITED_SENSOR_DESCRIPTIONS. This duplication is error-prone (future tweaks can easily land in only one list). Consider extracting these shared descriptions into a single list and concatenating it into both FULL and LIMITED lists.
| 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 |
There was a problem hiding this comment.
In async_setup_entry(), ev_subentry_serials is rebuilt inside the inverter-subentry loop for every inverter. Since it only depends on entry.subentries, it can be computed once before iterating to avoid repeated work and keep the setup logic consistent with the other platforms.
| 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} " |
There was a problem hiding this comment.
control_ev() parses direction into parsed_direction for validation, but then still passes the original direction to remoteControlEvCharger(). This makes parsed_direction effectively dead code and risks sending an unexpected type (int vs str) to the API. Consider changing the method signature to accept an int direction and consistently pass the validated/parsed value to the API call and log output (or remove the parse if the API must receive the original type).
| 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.""" |
There was a problem hiding this comment.
Both build_inverter_device_info() and build_ev_charger_device_info() accept a coordinator argument but do not use it. This adds noise at each call site and can confuse future readers about hidden dependencies. Either remove the parameter entirely, or rename it to _coordinator to make the intent explicit.
…fo` by removing unused coordinator parameter, deduplicate common sensor definitions.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…rger handling, and update sensor state classes to `TOTAL_INCREASING`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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) |
There was a problem hiding this comment.
build_ev_charger_device_info is defined to take (coordinator, data), but here it's called with only data, which will raise a TypeError at runtime when EV entities are set up. Align the call sites and device.py signatures (either pass coordinator here, or drop the unused coordinator parameter from the helper and update all callers consistently).
| ev_device_info = build_ev_charger_device_info(data) | |
| ev_device_info = build_ev_charger_device_info(coordinator, data) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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): |
There was a problem hiding this comment.
_cleanup_stale_ev_entities() runs unconditionally on first startup. If the coordinator is currently running in local-fallback mode (cloud_available == False), EV-related keys (including EV charger serial / connector powers) are intentionally absent, which would cause this migration to remove valid EV entities from the registry permanently. Gate the cleanup on cloud availability (and/or confirmed EV charger presence) so it only removes entities when authoritative EV cloud data is present.
| if not entry.options.get("_ev_entity_cleanup_done", False): | |
| cloud_available = getattr(_coordinator, "cloud_available", True) | |
| if cloud_available and not entry.options.get("_ev_entity_cleanup_done", False): |
…daily energy sensors
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
good to go!~ |
Added daily history sensors sourced from
getOneDateEnergyBySn/OneDateEnergy:(The data is grabbed from your current date/timezone)
Added one currency diagnostic sensor:
Currency Code (directly mapped from
SumData.moneyType)Configurable scan interval in options flow
Add charger diagnostics (e.g., whether the charger is able to charge). This makes it much easier to incorporate into automations, particularly for users who schedule charging based on energy pricing, solar generation, or battery levels.