From ca0661ed9d6985c19b7f286ae743fb3541780435 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 18:59:15 +0930 Subject: [PATCH 01/19] Make sensorlist file easier to understand --- custom_components/alphaess/sensorlist.py | 964 ++++++++--------------- 1 file changed, 337 insertions(+), 627 deletions(-) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 623cdb1..bc704a7 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -1,141 +1,147 @@ -from typing import List +""" +AlphaESS Home Assistant Integration - Sensor Definitions + +This module defines all sensor, button, and number entity descriptions for the AlphaESS integration. +Sensors are organized by category and access level (full vs limited API access). +""" + +from typing import List, Dict, Set +from dataclasses import dataclass from homeassistant.components.sensor import ( SensorDeviceClass, SensorStateClass, ) -from homeassistant.const import UnitOfEnergy, PERCENTAGE, UnitOfPower, CURRENCY_DOLLAR, EntityCategory, UnitOfMass +from homeassistant.const import ( + UnitOfEnergy, + PERCENTAGE, + UnitOfPower, + CURRENCY_DOLLAR, + EntityCategory, + UnitOfMass +) -from .entity import AlphaESSSensorDescription, AlphaESSButtonDescription, AlphaESSNumberDescription +from .entity import ( + AlphaESSSensorDescription, + AlphaESSButtonDescription, + AlphaESSNumberDescription +) from .enums import AlphaESSNames -FULL_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ - AlphaESSSensorDescription( - key=AlphaESSNames.SolarProduction, - name="Solar Production", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.SolarToBattery, - name="Solar to Battery", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.SolarToGrid, - name="Solar to Grid", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.SolarToLoad, - name="Solar to Load", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.TotalLoad, - name="Total Load", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridToLoad, - name="Grid to Load", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridToBattery, - name="Grid to Battery", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Charge, - name="Charge", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Discharge, - name="Discharge", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.EVCharger, - name="EV Charger", + +# ============================================================================ +# SENSOR CATEGORIES +# ============================================================================ + +def _create_energy_sensor(key: AlphaESSNames, name: str, + increasing: bool = True) -> AlphaESSSensorDescription: + """Helper to create energy sensors (kWh, total increasing).""" + return AlphaESSSensorDescription( + key=key, + name=name, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Generation, - name="Instantaneous Generation", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PPV1, - name="Instantaneous PPV1", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PPV2, - name="Instantaneous PPV2", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PPV3, - name="Instantaneous PPV3", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PPV4, - name="Instantaneous PPV4", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridIOL1, - name="Instantaneous Grid I/O L1", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridIOL2, - name="Instantaneous Grid I/O L2", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridIOL3, - name="Instantaneous Grid I/O L3", + state_class=SensorStateClass.TOTAL_INCREASING if increasing else SensorStateClass.TOTAL, + ) + + +def _create_power_sensor(key: AlphaESSNames, name: str, + icon: str = "mdi:flash") -> AlphaESSSensorDescription: + """Helper to create instantaneous power sensors (W).""" + return AlphaESSSensorDescription( + key=key, + name=name, native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - ), + icon=icon, + ) + + +def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, + unit: str = None, + device_class: str = None) -> AlphaESSSensorDescription: + """Helper to create diagnostic sensors.""" + return AlphaESSSensorDescription( + key=key, + name=name, + icon=icon, + native_unit_of_measurement=unit, + device_class=device_class, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ) + + +# ============================================================================ +# ENERGY FLOW SENSORS - Track energy movement between components +# ============================================================================ + +ENERGY_FLOW_SENSORS = [ + # Solar energy distribution + _create_energy_sensor(AlphaESSNames.SolarProduction, "Solar Production"), + _create_energy_sensor(AlphaESSNames.SolarToBattery, "Solar to Battery"), + _create_energy_sensor(AlphaESSNames.SolarToGrid, "Solar to Grid"), + _create_energy_sensor(AlphaESSNames.SolarToLoad, "Solar to Load"), + + # Grid interactions + _create_energy_sensor(AlphaESSNames.GridToLoad, "Grid to Load"), + _create_energy_sensor(AlphaESSNames.GridToBattery, "Grid to Battery"), + + # Battery operations + _create_energy_sensor(AlphaESSNames.Charge, "Charge"), + _create_energy_sensor(AlphaESSNames.Discharge, "Discharge"), + + # Consumption + _create_energy_sensor(AlphaESSNames.TotalLoad, "Total Load"), + _create_energy_sensor(AlphaESSNames.EVCharger, "EV Charger"), + + # Totals + _create_energy_sensor(AlphaESSNames.Total_Generation, "Total Generation"), +] + +# ============================================================================ +# INSTANTANEOUS POWER SENSORS - Real-time power measurements +# ============================================================================ + +INSTANTANEOUS_POWER_SENSORS = [ + # Generation + _create_power_sensor(AlphaESSNames.Generation, "Instantaneous Generation"), + + # Individual PV strings (only in FULL access) + _create_power_sensor(AlphaESSNames.PPV1, "Instantaneous PPV1"), + _create_power_sensor(AlphaESSNames.PPV2, "Instantaneous PPV2"), + _create_power_sensor(AlphaESSNames.PPV3, "Instantaneous PPV3"), + _create_power_sensor(AlphaESSNames.PPV4, "Instantaneous PPV4"), + + # Grid phases (only in FULL access) + _create_power_sensor(AlphaESSNames.GridIOL1, "Instantaneous Grid I/O L1"), + _create_power_sensor(AlphaESSNames.GridIOL2, "Instantaneous Grid I/O L2"), + _create_power_sensor(AlphaESSNames.GridIOL3, "Instantaneous Grid I/O L3"), + + # Totals (available in both FULL and LIMITED) + _create_power_sensor(AlphaESSNames.GridIOTotal, "Instantaneous Grid I/O Total"), + _create_power_sensor(AlphaESSNames.Load, "Instantaneous Load"), + + # Battery + _create_power_sensor(AlphaESSNames.BatteryIO, "Instantaneous Battery I/O"), + + # DC meter + _create_power_sensor(AlphaESSNames.pmeterDc, "pmeterDc", "mdi:current-dc"), + + # Unknown purpose + _create_power_sensor(AlphaESSNames.pev, "pev"), + _create_power_sensor(AlphaESSNames.PrealL1, "PrealL1"), + _create_power_sensor(AlphaESSNames.PrealL2, "PrealL2"), + _create_power_sensor(AlphaESSNames.PrealL3, "PrealL3"), +] + +# ============================================================================ +# BATTERY SENSORS - Battery state and configuration +# ============================================================================ + +BATTERY_SENSORS = [ + # Battery state AlphaESSSensorDescription( key=AlphaESSNames.BatterySOC, name="Instantaneous Battery SOC", @@ -143,34 +149,52 @@ device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), + + # Alternative SOC (only in LIMITED access) AlphaESSSensorDescription( - key=AlphaESSNames.BatteryIO, - name="Instantaneous Battery I/O", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridIOTotal, - name="Instantaneous Grid I/O Total", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, + key=AlphaESSNames.StateOfCharge, + name="State of Charge", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), - AlphaESSSensorDescription( - key=AlphaESSNames.Load, - name="Instantaneous Load", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, + + # Battery capacity info + _create_diagnostic_sensor( + AlphaESSNames.usCapacity, + "Maximum Battery Capacity", + "mdi:home-percent", + PERCENTAGE + ), + _create_diagnostic_sensor( + AlphaESSNames.cobat, + "Installed Capacity", + "mdi:battery-heart-variant", + UnitOfEnergy.KILO_WATT_HOUR, + SensorDeviceClass.ENERGY + ), + _create_diagnostic_sensor( + AlphaESSNames.surplusCobat, + "Current Capacity", + "mdi:battery-heart-variant", + UnitOfEnergy.KILO_WATT_HOUR, + SensorDeviceClass.ENERGY ), - AlphaESSSensorDescription( - key=AlphaESSNames.Total_Generation, - name="Total Generation", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, + + # Battery model + _create_diagnostic_sensor( + AlphaESSNames.mbat, + "Battery Model", + "mdi:battery-heart-variant" ), +] + +# ============================================================================ +# SYSTEM STATUS & PERFORMANCE SENSORS +# ============================================================================ + +SYSTEM_STATUS_SENSORS = [ + # Financial AlphaESSSensorDescription( key=AlphaESSNames.Income, name="Total Income", @@ -179,6 +203,8 @@ device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), + + # Self-sufficiency metrics AlphaESSSensorDescription( key=AlphaESSNames.SelfConsumption, name="Self Consumption", @@ -195,176 +221,32 @@ device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), - AlphaESSSensorDescription( - key=AlphaESSNames.EmsStatus, - name="EMS Status", - icon="mdi:home-battery", - device_class=SensorDeviceClass.ENUM, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC - ), - AlphaESSSensorDescription( - key=AlphaESSNames.usCapacity, - name="Maximum Battery Capacity", - icon="mdi:home-percent", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.TOTAL, - entity_category=EntityCategory.DIAGNOSTIC - ), - AlphaESSSensorDescription( - key=AlphaESSNames.cobat, - name="Installed Capacity", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC - ), - AlphaESSSensorDescription( - key=AlphaESSNames.surplusCobat, - name="Current Capacity", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ChargeTime1, - name="Charge Time 1", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ChargeTime2, - name="Charge Time 2", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DischargeTime1, - name="Discharge Time 1", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.DischargeTime2, - name="Discharge Time 2", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ChargeRange, - name="Charging Range", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:battery-lock-open", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.mbat, - name="Battery Model", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:battery-heart-variant", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.poinv, - name="Inverter nominal Power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=None, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:lightning-bolt", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.popv, - name="Pv nominal Power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=None, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:lightning-bolt", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.pmeterDc, - name="pmeterDc", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:current-dc", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerOne, - name="Electric Vehicle Power One", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerTwo, - name="Electric Vehicle Power Two", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerThree, - name="Electric Vehicle Power Three", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerFour, - name="Electric Vehicle Power Four", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.pev, - name="pev", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PrealL1, - name="PrealL1", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PrealL2, - name="PrealL2", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", + + # System status + _create_diagnostic_sensor( + AlphaESSNames.EmsStatus, + "EMS Status", + "mdi:home-battery", + device_class=SensorDeviceClass.ENUM ), - AlphaESSSensorDescription( - key=AlphaESSNames.PrealL3, - name="PrealL3", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", + + # System specs + _create_diagnostic_sensor( + AlphaESSNames.poinv, + "Inverter nominal Power", + "mdi:lightning-bolt", + UnitOfEnergy.KILO_WATT_HOUR, + SensorDeviceClass.ENERGY + ), + _create_diagnostic_sensor( + AlphaESSNames.popv, + "Pv nominal Power", + "mdi:lightning-bolt", + UnitOfEnergy.KILO_WATT_HOUR, + SensorDeviceClass.ENERGY ), + + # Environmental impact AlphaESSSensorDescription( key=AlphaESSNames.carbonReduction, name="Co2 Reduction", @@ -379,305 +261,102 @@ native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, icon="mdi:tree", - ) + ), ] -LIMITED_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ - AlphaESSSensorDescription( - key=AlphaESSNames.StateOfCharge, - name="State of Charge", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.SolarProduction, - name="Solar Production", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.SolarToBattery, - name="Solar to Battery", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.TotalLoad, - name="Total Load", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridToLoad, - name="Grid to Load", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridToBattery, - name="Grid to Battery", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Charge, - name="Charge", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Discharge, - name="Discharge", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.EVCharger, - name="EV Charger", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.GridIOTotal, - name="Instantaneous Grid I/O Total", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Load, - name="Instantaneous Load", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Total_Generation, - name="Total Generation", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.Income, - name="Total Income", - icon="mdi:cash-multiple", - native_unit_of_measurement=CURRENCY_DOLLAR, - device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.SelfConsumption, - name="Self Consumption", - icon="mdi:home-percent", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.POWER_FACTOR, - state_class=SensorStateClass.MEASUREMENT, +# ============================================================================ +# SCHEDULING SENSORS - Charge/discharge time configurations +# ============================================================================ + +SCHEDULING_SENSORS = [ + _create_diagnostic_sensor( + AlphaESSNames.ChargeTime1, "Charge Time 1", "mdi:clock-time-ten" ), - AlphaESSSensorDescription( - key=AlphaESSNames.SelfSufficiency, - name="Self Sufficiency", - icon="mdi:home-percent", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.POWER_FACTOR, - state_class=SensorStateClass.MEASUREMENT, + _create_diagnostic_sensor( + AlphaESSNames.ChargeTime2, "Charge Time 2", "mdi:clock-time-ten" ), - AlphaESSSensorDescription( - key=AlphaESSNames.EmsStatus, - name="EMS Status", - icon="mdi:home-battery", - device_class=SensorDeviceClass.ENUM, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC + _create_diagnostic_sensor( + AlphaESSNames.DischargeTime1, "Discharge Time 1", "mdi:clock-time-ten" ), - AlphaESSSensorDescription( - key=AlphaESSNames.usCapacity, - name="Maximum Battery Capacity", - icon="mdi:home-percent", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.TOTAL, - entity_category=EntityCategory.DIAGNOSTIC + _create_diagnostic_sensor( + AlphaESSNames.DischargeTime2, "Discharge Time 2", "mdi:clock-time-ten" ), - AlphaESSSensorDescription( - key=AlphaESSNames.cobat, - name="Installed Capacity", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC - ), - AlphaESSSensorDescription( - key=AlphaESSNames.surplusCobat, - name="Current Capacity", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC + _create_diagnostic_sensor( + AlphaESSNames.ChargeRange, "Charging Range", "mdi:battery-lock-open" ), - AlphaESSSensorDescription( - key=AlphaESSNames.ChargeTime1, - name="Charge Time 1", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ChargeTime2, - name="Charge Time 2", - native_unit_of_measurement=None, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", +] + +# ============================================================================ +# EV CHARGER SENSORS +# ============================================================================ + +EV_POWER_SENSORS = [ + _create_power_sensor( + AlphaESSNames.ElectricVehiclePowerOne, + "Electric Vehicle Power One", + "mdi:car-electric" + ), + _create_power_sensor( + AlphaESSNames.ElectricVehiclePowerTwo, + "Electric Vehicle Power Two", + "mdi:car-electric" + ), + _create_power_sensor( + AlphaESSNames.ElectricVehiclePowerThree, + "Electric Vehicle Power Three", + "mdi:car-electric" + ), + _create_power_sensor( + AlphaESSNames.ElectricVehiclePowerFour, + "Electric Vehicle Power Four", + "mdi:car-electric" ), +] + +EV_CHARGING_DETAILS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( - key=AlphaESSNames.DischargeTime1, - name="Discharge Time 1", + key=AlphaESSNames.evchargersn, + name="EV Charger S/N", + icon="mdi:ev-station", native_unit_of_measurement=None, state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", ), AlphaESSSensorDescription( - key=AlphaESSNames.DischargeTime2, - name="Discharge Time 2", + key=AlphaESSNames.evchargermodel, + name="EV Charger Model", + icon="mdi:ev-station", native_unit_of_measurement=None, state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:clock-time-ten", ), AlphaESSSensorDescription( - key=AlphaESSNames.ChargeRange, - name="Charging Range", + key=AlphaESSNames.evchargerstatusraw, + name="EV Charger Status Raw", + icon="mdi:ev-station", native_unit_of_measurement=None, state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:battery-lock-open", ), AlphaESSSensorDescription( - key=AlphaESSNames.mbat, - name="Battery Model", + key=AlphaESSNames.evchargerstatus, + name="EV Charger Status", + icon="mdi:ev-station", + device_class="enum", native_unit_of_measurement=None, state_class=None, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:battery-heart-variant", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.poinv, - name="Inverter nominal Power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=None, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:lightning-bolt", ), AlphaESSSensorDescription( - key=AlphaESSNames.popv, - name="Pv nominal Power", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + key=AlphaESSNames.evcurrentsetting, + name="Household current setup", + icon="mdi:ev-station", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement="A", state_class=None, - device_class=SensorDeviceClass.ENERGY, - entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:lightning-bolt", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.pmeterDc, - name="pmeterDc", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:current-dc", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerOne, - name="Electric Vehicle Power One", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerTwo, - name="Electric Vehicle Power Two", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerThree, - name="Electric Vehicle Power Three", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.ElectricVehiclePowerFour, - name="Electric Vehicle Power Four", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:car-electric", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.pev, - name="pev", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PrealL1, - name="PrealL1", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PrealL2, - name="PrealL2", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.PrealL3, - name="PrealL3", - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:flash", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.carbonReduction, - name="Carbon Reduction", - native_unit_of_measurement=UnitOfMass.KILOGRAMS, - device_class=SensorDeviceClass.WEIGHT, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:molecule-co2", - ), - AlphaESSSensorDescription( - key=AlphaESSNames.treePlanted, - name="Trees Planted", - native_unit_of_measurement=None, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:tree", ) ] +# ============================================================================ +# CONTROL ENTITIES - Buttons and Numbers +# ============================================================================ + SUPPORT_DISCHARGE_AND_CHARGE_BUTTON_DESCRIPTIONS: List[AlphaESSButtonDescription] = [ AlphaESSButtonDescription( key=AlphaESSNames.ButtonDischargeFifteen, @@ -730,7 +409,6 @@ entity_category=EntityCategory.CONFIG, icon="mdi:battery-sync", native_unit_of_measurement=PERCENTAGE, - ), AlphaESSNumberDescription( key=AlphaESSNames.batUseCap, @@ -741,8 +419,7 @@ ) ] -EV_DISCHARGE_AND_CHARGE_BUTTONS: List[AlphaESSNumberDescription] = [ - +EV_DISCHARGE_AND_CHARGE_BUTTONS: List[AlphaESSButtonDescription] = [ AlphaESSButtonDescription( key=AlphaESSNames.stopcharging, name="Stop Charging", @@ -755,46 +432,79 @@ icon="mdi:battery-plus", entity_category=EntityCategory.CONFIG, ) - ] -EV_CHARGING_DETAILS: List[AlphaESSSensorDescription] = [ - AlphaESSSensorDescription( - key=AlphaESSNames.evchargersn, - name="EV Charger S/N", - icon="mdi:ev-station", - native_unit_of_measurement=None, - state_class=None, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.evchargermodel, - name="EV Charger Model", - icon="mdi:ev-station", - native_unit_of_measurement=None, - state_class=None, - ), - AlphaESSSensorDescription( - key=AlphaESSNames.evchargerstatusraw, - name="EV Charger Status Raw", - icon="mdi:ev-station", - native_unit_of_measurement=None, - state_class=None, - ), +# ============================================================================ +# SENSOR COLLECTIONS - Full vs Limited API Access +# ============================================================================ + +# Sensors exclusive to FULL API access +FULL_ONLY_SENSORS = [ + # Individual PV string monitoring + sensor for sensor in INSTANTANEOUS_POWER_SENSORS + if sensor.key in [ + AlphaESSNames.PPV1, AlphaESSNames.PPV2, + AlphaESSNames.PPV3, AlphaESSNames.PPV4 + ] + ] + [ + # Individual grid phase monitoring + sensor for sensor in INSTANTANEOUS_POWER_SENSORS + if sensor.key in [ + AlphaESSNames.GridIOL1, AlphaESSNames.GridIOL2, + AlphaESSNames.GridIOL3 + ] + ] + [ + # Solar to grid energy flow + _create_energy_sensor(AlphaESSNames.SolarToGrid, "Solar to Grid"), + _create_energy_sensor(AlphaESSNames.SolarToLoad, "Solar to Load"), + + # Additional sensors only in full + _create_power_sensor(AlphaESSNames.Generation, "Instantaneous Generation"), + AlphaESSSensorDescription( + key=AlphaESSNames.BatterySOC, + name="Instantaneous Battery SOC", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + ), + _create_power_sensor(AlphaESSNames.BatteryIO, "Instantaneous Battery I/O"), + ] + +# Sensors exclusive to LIMITED API access +LIMITED_ONLY_SENSORS = [ AlphaESSSensorDescription( - key=AlphaESSNames.evchargerstatus, - name="EV Charger Status", - icon="mdi:ev-station", - device_class="enum", - native_unit_of_measurement=None, - state_class=None, + key=AlphaESSNames.StateOfCharge, + name="State of Charge", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), - AlphaESSSensorDescription( - key=AlphaESSNames.evcurrentsetting, - name="Household current setup", - icon="mdi:ev-station", - device_class=SensorDeviceClass.CURRENT, - native_unit_of_measurement="A", - state_class=None, - ) - ] + +# Common sensors available in both FULL and LIMITED +COMMON_SENSORS = ( + ENERGY_FLOW_SENSORS + + SYSTEM_STATUS_SENSORS + + SCHEDULING_SENSORS + + EV_POWER_SENSORS + + [sensor for sensor in BATTERY_SENSORS if sensor.key != AlphaESSNames.StateOfCharge] + + [sensor for sensor in INSTANTANEOUS_POWER_SENSORS + if sensor.key in [ + AlphaESSNames.GridIOTotal, AlphaESSNames.Load, + AlphaESSNames.pmeterDc, AlphaESSNames.pev, + AlphaESSNames.PrealL1, AlphaESSNames.PrealL2, AlphaESSNames.PrealL3 + ]] +) + +# Remove duplicates from COMMON_SENSORS +full_only_keys = {sensor.key for sensor in FULL_ONLY_SENSORS} +COMMON_SENSORS = [sensor for sensor in COMMON_SENSORS if sensor.key not in full_only_keys] + +# Final collections +FULL_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = ( + COMMON_SENSORS + FULL_ONLY_SENSORS +) + +LIMITED_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = ( + COMMON_SENSORS + LIMITED_ONLY_SENSORS +) From a20928b4b0e818c6ba2161dffc9d58837aa40eb7 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 19:42:08 +0930 Subject: [PATCH 02/19] Fixes for missing state classes --- custom_components/alphaess/sensorlist.py | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index bc704a7..a2e372c 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -60,7 +60,8 @@ def _create_power_sensor(key: AlphaESSNames, name: str, def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, unit: str = None, - device_class: str = None) -> AlphaESSSensorDescription: + device_class: str = None, + state_class: SensorStateClass = None) -> AlphaESSSensorDescription: """Helper to create diagnostic sensors.""" return AlphaESSSensorDescription( key=key, @@ -68,7 +69,7 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, icon=icon, native_unit_of_measurement=unit, device_class=device_class, - state_class=None, + state_class=state_class, entity_category=EntityCategory.DIAGNOSTIC, ) @@ -164,21 +165,24 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, AlphaESSNames.usCapacity, "Maximum Battery Capacity", "mdi:home-percent", - PERCENTAGE + PERCENTAGE, + state_class=SensorStateClass.TOTAL ), _create_diagnostic_sensor( AlphaESSNames.cobat, "Installed Capacity", "mdi:battery-heart-variant", UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL ), _create_diagnostic_sensor( AlphaESSNames.surplusCobat, "Current Capacity", "mdi:battery-heart-variant", UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY + SensorDeviceClass.ENERGY, + SensorStateClass.TOTAL ), # Battery model @@ -223,11 +227,13 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, ), # System status - _create_diagnostic_sensor( - AlphaESSNames.EmsStatus, - "EMS Status", - "mdi:home-battery", - device_class=SensorDeviceClass.ENUM + AlphaESSSensorDescription( + key=AlphaESSNames.EmsStatus, + name="EMS Status", + icon="mdi:home-battery", + device_class=SensorDeviceClass.ENUM, + state_class=None, # ENUM sensors cannot have a state_class + entity_category=EntityCategory.DIAGNOSTIC ), # System specs From bddc82856295f82c6ec8cae5709d1edcdd23adaa Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 20:01:43 +0930 Subject: [PATCH 03/19] Rework the coordinator to parse data via groups (API call groups) --- custom_components/alphaess/coordinator.py | 519 +++++++++++++--------- 1 file changed, 321 insertions(+), 198 deletions(-) diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index ad78d39..b67693c 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -1,6 +1,7 @@ """Coordinator for AlphaEss integration.""" import logging from datetime import datetime, timedelta +from typing import Any, Dict, Optional, Union import aiohttp from alphaess import alphaess @@ -8,243 +9,365 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, SCAN_INTERVAL, THROTTLE_MULTIPLIER, get_inverter_count, set_throttle_count_lower, \ - get_inverter_list, LOWER_INVERTER_API_CALL_LIST +from .const import ( + DOMAIN, + SCAN_INTERVAL, + THROTTLE_MULTIPLIER, + get_inverter_count, + set_throttle_count_lower, + get_inverter_list, + LOWER_INVERTER_API_CALL_LIST +) _LOGGER: logging.Logger = logging.getLogger(__package__) -async def process_value(value, default=None): - if value is None or (isinstance(value, str) and value.strip() == ''): - return default - return value - - -async def safe_get(dictionary, key, default=None): - if dictionary is None: - return default - return await process_value(dictionary.get(key), default) +class DataProcessor: + """Helper class for data processing utilities.""" + + @staticmethod + async def process_value(value: Any, default: Any = None) -> Any: + """Process and validate a value, returning default if empty.""" + if value is None or (isinstance(value, str) and value.strip() == ''): + return default + return value + + @staticmethod + async def safe_get(dictionary: Optional[Dict], key: str, default: Any = None) -> Any: + """Safely get a value from a dictionary.""" + if dictionary is None: + return default + return await DataProcessor.process_value(dictionary.get(key), default) + + @staticmethod + async def safe_calculate(val1: Optional[float], val2: Optional[float]) -> Optional[float]: + """Safely calculate difference between two values.""" + if val1 is None or val2 is None: + return None + return val1 - val2 -async def safe_calculate(val1, val2): - if val1 is None or val2 is None: - return None - else: - return val1 - val2 +class TimeHelper: + """Helper class for time-related operations.""" + @staticmethod + async def get_rounded_time() -> str: + """Get time rounded to next 15-minute interval.""" + now = datetime.now() -async def get_rounded_time(): - now = datetime.now() + if now.minute > 45: + rounded_time = now + timedelta(hours=1) + rounded_time = rounded_time.replace(minute=0, second=0, microsecond=0) + else: + rounded_time = now + timedelta(minutes=15 - (now.minute % 15)) + rounded_time = rounded_time.replace(second=0, microsecond=0) - if now.minute > 45: - rounded_time = now + timedelta(hours=1) - rounded_time = rounded_time.replace(minute=0, second=0, microsecond=0) - else: - rounded_time = now + timedelta(minutes=15 - (now.minute % 15)) - rounded_time = rounded_time.replace(second=0, microsecond=0) + return rounded_time.strftime("%H:%M") - return rounded_time.strftime("%H:%M") + @staticmethod + def calculate_time_window(time_period_minutes: int) -> tuple[str, str]: + """Calculate start and end time for a given period.""" + now = datetime.now() + start_time_str = TimeHelper.get_rounded_time() + start_time = datetime.strptime(start_time_str, "%H:%M").replace( + year=now.year, month=now.month, day=now.day + ) + end_time = start_time + timedelta(minutes=time_period_minutes) + return start_time.strftime("%H:%M"), end_time.strftime("%H:%M") + + +class InverterDataParser: + """Parse inverter data into structured format.""" + + def __init__(self, data_processor: DataProcessor): + self.dp = data_processor + + async def parse_basic_info(self, invertor: Dict) -> Dict[str, Any]: + """Parse basic inverter information.""" + return { + "Model": await self.dp.process_value(invertor.get("minv")), + "Battery Model": await self.dp.process_value(invertor.get("mbat")), + "Inverter nominal Power": await self.dp.process_value(invertor.get("poinv")), + "Pv nominal Power": await self.dp.process_value(invertor.get("popv")), + "EMS Status": await self.dp.process_value(invertor.get("emsStatus")), + "Maximum Battery Capacity": await self.dp.process_value(invertor.get("usCapacity")), + "Current Capacity": await self.dp.process_value(invertor.get("surplusCobat")), + "Installed Capacity": await self.dp.process_value(invertor.get("cobat")), + } + + async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[str, Any]: + """Parse EV charger data.""" + if not ev_data: + return {} + + ev_data = ev_data[0] if isinstance(ev_data, list) else ev_data + ev_status = invertor.get("EVStatus", {}) + ev_current = invertor.get("EVCurrent", {}) + + return { + "EV Charger S/N": await self.dp.safe_get(ev_data, "evchargerSn"), + "EV Charger Model": await self.dp.safe_get(ev_data, "evchargerModel"), + "EV Charger Status": await self.dp.safe_get(ev_status, "evchargerStatus"), + "EV Charger Status Raw": await self.dp.safe_get(ev_status, "evchargerStatus"), + "Household current setup": await self.dp.safe_get(ev_current, "currentsetting"), + } + + async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: + """Parse summary statistics.""" + data = { + "Total Load": await self.dp.safe_get(sum_data, "eload"), + "Total Income": await self.dp.safe_get(sum_data, "totalIncome"), + "Total Generation": await self.dp.safe_get(sum_data, "epvtotal"), + "Trees Planted": await self.dp.safe_get(sum_data, "treeNum"), + "Co2 Reduction": await self.dp.safe_get(sum_data, "carbonNum"), + "Currency": await self.dp.safe_get(sum_data, "moneyType"), + } + + # Convert percentages + for key in ["eselfConsumption", "eselfSufficiency"]: + value = await self.dp.safe_get(sum_data, key) + readable_key = key.replace("e", "").replace("selfC", "Self C").replace("selfS", "Self S") + data[readable_key] = value * 100 if value is not None else None + + return data + + async def parse_energy_data(self, energy_data: Dict) -> Dict[str, Any]: + """Parse daily energy flow data.""" + pv = await self.dp.safe_get(energy_data, "epv") + 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") + + return { + "Solar Production": pv, + "Solar to Load": await self.dp.safe_calculate(pv, feedin), + "Solar to Grid": feedin, + "Solar to Battery": await self.dp.safe_calculate(charge, gridcharge), + "Grid to Load": await self.dp.safe_get(energy_data, "eInput"), + "Grid to Battery": gridcharge, + "Charge": charge, + "Discharge": await self.dp.safe_get(energy_data, "eDischarge"), + "EV Charger": await self.dp.safe_get(energy_data, "eChargingPile"), + } + + async def parse_power_data(self, power_data: Dict, one_day_power: Optional[list]) -> Dict[str, Any]: + """Parse instantaneous power data.""" + soc = await self.dp.safe_get(power_data, "soc") + grid_details = power_data.get("pgridDetail", {}) + pv_details = power_data.get("ppvDetail", {}) + ev_details = power_data.get("pevDetail", {}) + + data = { + "Instantaneous Battery SOC": soc, + "Instantaneous Battery I/O": await self.dp.safe_get(power_data, "pbat"), + "Instantaneous Load": await self.dp.safe_get(power_data, "pload"), + "Instantaneous Generation": await self.dp.safe_get(power_data, "ppv"), + "Instantaneous Grid I/O Total": await self.dp.safe_get(power_data, "pgrid"), + "pev": await self.dp.safe_get(power_data, "pev"), + "PrealL1": await self.dp.safe_get(power_data, "prealL1"), + "PrealL2": await self.dp.safe_get(power_data, "prealL2"), + "PrealL3": await self.dp.safe_get(power_data, "prealL3"), + } + + # PV string data + for i in range(1, 5): + data[f"Instantaneous PPV{i}"] = await self.dp.safe_get(pv_details, f"ppv{i}") + + data["pmeterDc"] = await self.dp.safe_get(pv_details, "pmeterDc") + + # Grid phase data + for i in range(1, 4): + data[f"Instantaneous Grid I/O L{i}"] = await self.dp.safe_get(grid_details, f"pmeterL{i}") + + # EV power data + for i in range(1, 5): + key = ["One", "Two", "Three", "Four"][i - 1] + data[f"Electric Vehicle Power {key}"] = await self.dp.safe_get(ev_details, f"ev{i}Power") + + # Fallback SOC from daily data + if one_day_power and soc == 0: + first_entry = one_day_power[0] + cbat = first_entry.get("cbat") + if cbat is not None: + data["State of Charge"] = cbat + + return data + + async def parse_charge_config(self, config: Dict) -> Dict[str, Any]: + """Parse charge configuration.""" + data = {} + for key in ["gridCharge", "batHighCap"]: + data[key] = await self.dp.safe_get(config, key) + + # Parse time slots + for slot in [1, 2]: + data[f"charge_timeChaf{slot}"] = await self.dp.safe_get(config, f"timeChaf{slot}") + data[f"charge_timeChae{slot}"] = await self.dp.safe_get(config, f"timeChae{slot}") + + return data + + async def parse_discharge_config(self, config: Dict) -> Dict[str, Any]: + """Parse discharge configuration.""" + data = {} + for key in ["ctrDis", "batUseCap"]: + data[key] = await self.dp.safe_get(config, key) + + # Parse time slots + for slot in [1, 2]: + data[f"discharge_timeDisf{slot}"] = await self.dp.safe_get(config, f"timeDisf{slot}") + data[f"discharge_timeDise{slot}"] = await self.dp.safe_get(config, f"timeDise{slot}") + + return data class AlphaESSDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" def __init__(self, hass: HomeAssistant, client: alphaess.alphaess) -> None: - """Initialize.""" + """Initialize coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) self.api = client - self.update_method = self._async_update_data - self.has_throttle = True + self.hass = hass self.data: dict[str, dict[str, float]] = {} - self.LOCAL_INVERTER_COUNT = 0 + + # Initialize helpers + self.data_processor = DataProcessor() + self.time_helper = TimeHelper() + self.parser = InverterDataParser(self.data_processor) + + # Configure based on inverter types + self._configure_inverter_settings() + + def _configure_inverter_settings(self) -> None: + """Configure settings based on inverter types.""" self.model_list = get_inverter_list() self.inverter_count = get_inverter_count() - self.hass = hass + self.LOCAL_INVERTER_COUNT = 0 if self.inverter_count <= 1 else self.inverter_count - # Reduce the throttle count lower due to the reduced API calls it makes - if all(inverter not in self.model_list for inverter in LOWER_INVERTER_API_CALL_LIST) and len( - self.model_list) > 0: + # Check if we need reduced API calls + self.has_throttle = True + if (all(inverter not in self.model_list for inverter in LOWER_INVERTER_API_CALL_LIST) + and len(self.model_list) > 0): self.has_throttle = False set_throttle_count_lower() - if self.inverter_count <= 1: - self.LOCAL_INVERTER_COUNT = 0 - else: - self.LOCAL_INVERTER_COUNT = self.inverter_count - - async def Control_EV(self, serial, ev_serial, direction): - return_data = await self.api.remoteControlEvCharger(serial, ev_serial, direction) - _LOGGER.info(f"Control EV Charger: {ev_serial} for serial: {serial} Direction: {direction}") - _LOGGER.info(return_data) - - async def reset_config(self, serial): - batUseCap = self.hass.data[DOMAIN][serial].get("batUseCap", 10) - batHighCap = self.hass.data[DOMAIN][serial].get("batHighCap", 90) + async def control_ev(self, serial: str, ev_serial: str, direction: str) -> None: + """Control EV charger.""" + 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}" + ) - return_charge_data = await self.api.updateChargeConfigInfo(serial, batHighCap, 1, "00:00", "00:00", - "00:00", "00:00") - return_discharge_data = await self.api.updateDisChargeConfigInfo(serial, batUseCap, 1, "00:00", "00:00", - "00:00", "00:00") + async def reset_config(self, serial: str) -> None: + """Reset charge and discharge configuration.""" + bat_use_cap = self.hass.data[DOMAIN][serial].get("batUseCap", 10) + bat_high_cap = self.hass.data[DOMAIN][serial].get("batHighCap", 90) + results = await self._reset_charge_discharge_config(serial, bat_high_cap, bat_use_cap) _LOGGER.info( - f"Reset Charge and Discharge status, now is reset, API response:\n Charge: {return_charge_data}\n Discharge: {return_discharge_data}") + f"Reset Charge and Discharge configuration - " + f"Charge: {results['charge']}, Discharge: {results['discharge']}" + ) + + async def _reset_charge_discharge_config( + self, serial: str, bat_high_cap: int, bat_use_cap: int + ) -> Dict[str, Any]: + """Internal method to reset configurations.""" + charge_result = await self.api.updateChargeConfigInfo( + serial, bat_high_cap, 1, "00:00", "00:00", "00:00", "00:00" + ) + discharge_result = await self.api.updateDisChargeConfigInfo( + serial, bat_use_cap, 1, "00:00", "00:00", "00:00", "00:00" + ) + return {"charge": charge_result, "discharge": discharge_result} + + async def update_discharge(self, name: str, serial: str, time_period: int) -> None: + """Update discharge configuration for specified time period.""" + bat_use_cap = self.hass.data[DOMAIN][serial].get(name) + start_time, end_time = self.time_helper.calculate_time_window(time_period) + + result = await self.api.updateDisChargeConfigInfo( + serial, bat_use_cap, 1, end_time, "00:00", start_time, "00:00" + ) - async def update_discharge(self, name, serial, time_period): - batUseCap = self.hass.data[DOMAIN][serial].get(name, None) - start_time_str = await get_rounded_time() - now = datetime.now() - start_time = datetime.strptime(start_time_str, "%H:%M").replace(year=now.year, month=now.month, day=now.day) - future_time = start_time + timedelta(minutes=time_period) - future_time_str = future_time.strftime("%H:%M") - return_data = await self.api.updateDisChargeConfigInfo(serial, batUseCap, 1, future_time_str, "00:00", - start_time.strftime("%H:%M"), "00:00") _LOGGER.info( - f"Retrieved value for Discharge: {batUseCap} for serial: {serial} Running for {start_time.strftime('%H:%M')} to {future_time_str}") - _LOGGER.info(return_data) + f"Updated discharge config - Capacity: {bat_use_cap}, " + f"Period: {start_time} to {end_time}, Result: {result}" + ) - _LOGGER.info(f"DATA RECEIVED:{await self.api.getDisChargeConfigInfo(serial)}") + async def update_charge(self, name: str, serial: str, time_period: int) -> None: + """Update charge configuration for specified time period.""" + bat_high_cap = self.hass.data[DOMAIN][serial].get(name) + start_time, end_time = self.time_helper.calculate_time_window(time_period) - async def update_charge(self, name, serial, time_period): + result = await self.api.updateChargeConfigInfo( + serial, bat_high_cap, 1, end_time, "00:00", start_time, "00:00" + ) - batHighCap = self.hass.data[DOMAIN][serial].get(name, None) - start_time_str = await get_rounded_time() - now = datetime.now() - start_time = datetime.strptime(start_time_str, "%H:%M").replace(year=now.year, month=now.month, day=now.day) - future_time = start_time + timedelta(minutes=time_period) - future_time_str = future_time.strftime("%H:%M") - return_data = await self.api.updateChargeConfigInfo(serial, batHighCap, 1, future_time_str, "00:00", - start_time.strftime("%H:%M"), "00:00") _LOGGER.info( - f"Retrieved value for Charge: {batHighCap} for serial: {serial} Running from {start_time.strftime('%H:%M')} to {future_time_str}") - _LOGGER.info(return_data) + f"Updated charge config - Capacity: {bat_high_cap}, " + f"Period: {start_time} to {end_time}, Result: {result}" + ) - async def _async_update_data(self): + async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: """Update data via library.""" try: - jsondata = await self.api.getdata(True, True, THROTTLE_MULTIPLIER * self.LOCAL_INVERTER_COUNT) - if jsondata is not None: - for invertor in jsondata: - - # data from system list data - inverterdata = {} - if invertor.get("minv") is not None: - inverterdata["Model"] = await process_value(invertor.get("minv")) - - if invertor.get("mbat") is not None: - inverterdata["Battery Model"] = await process_value(invertor.get("mbat")) - - inverterdata["Inverter nominal Power"] = await process_value(invertor.get("poinv")) - inverterdata["Pv nominal Power"] = await process_value(invertor.get("popv")) - - inverterdata["EMS Status"] = await process_value(invertor.get("emsStatus")) - inverterdata["Maximum Battery Capacity"] = await process_value(invertor.get("usCapacity")) - inverterdata["Current Capacity"] = await process_value(invertor.get("surplusCobat")) - inverterdata["Installed Capacity"] = await process_value(invertor.get("cobat")) - - _sumdata = invertor.get("SumData", {}) - _onedateenergy = invertor.get("OneDateEnergy", {}) - _powerdata = invertor.get("LastPower", {}) - _onedatepower = invertor.get("OneDayPower", {}) - _evdata = invertor.get("EVData", {}) - - if _evdata: - _evdata = _evdata[0] - inverterdata["EV Charger S/N"] = await safe_get(_evdata, "evchargerSn") - inverterdata["EV Charger Model"] = await safe_get(_evdata, "evchargerModel") - _evstatus = invertor.get("EVStatus", {}) - inverterdata["EV Charger Status"] = await safe_get(_evstatus, "evchargerStatus") - inverterdata["EV Charger Status Raw"] = await safe_get(_evstatus, "evchargerStatus") - _evcurrent = invertor.get("EVCurrent", {}) - inverterdata["Household current setup"] = await safe_get(_evcurrent, "currentsetting") - - inverterdata["Total Load"] = await safe_get(_sumdata, "eload") - inverterdata["Total Income"] = await safe_get(_sumdata, "totalIncome") - inverterdata["Total Generation"] = await safe_get(_sumdata, "epvtotal") - inverterdata["Trees Planted"] = await safe_get(_sumdata, "treeNum") - inverterdata["Co2 Reduction"] = await safe_get(_sumdata, "carbonNum") - - self_data = { - "Self Consumption": await safe_get(_sumdata, "eselfConsumption"), - "Self Sufficiency": await safe_get(_sumdata, "eselfSufficiency") - } - - inverterdata["Currency"] = await safe_get(_sumdata, "moneyType") - - for key, value in self_data.items(): - inverterdata[key] = value * 100 if value is not None else None - - _pv = await safe_get(_onedateenergy, "epv") - _feedin = await safe_get(_onedateenergy, "eOutput") - _gridcharge = await safe_get(_onedateenergy, "eGridCharge") - _charge = await safe_get(_onedateenergy, "eCharge") - - inverterdata["Solar Production"] = _pv - inverterdata["Solar to Load"] = await safe_calculate(_pv, _feedin) - inverterdata["Solar to Grid"] = _feedin - inverterdata["Solar to Battery"] = await safe_calculate(_charge, _gridcharge) - inverterdata["Grid to Load"] = await safe_get(_onedateenergy, "eInput") - inverterdata["Grid to Battery"] = _gridcharge - inverterdata["Charge"] = _charge - inverterdata["Discharge"] = await safe_get(_onedateenergy, "eDischarge") - inverterdata["EV Charger"] = await safe_get(_onedateenergy, "eChargingPile") - - _soc = await safe_get(_powerdata, "soc") - _gridpowerdetails = _powerdata.get("pgridDetail", {}) - _pvpowerdetails = _powerdata.get("ppvDetail", {}) - _getEVdetails = _powerdata.get("pevDetail", {}) - - inverterdata["Instantaneous Battery SOC"] = _soc - - if _onedatepower and _soc == 0: - first_entry = _onedatepower[0] - _cbat = first_entry.get("cbat", None) - inverterdata["State of Charge"] = _cbat - - inverterdata["Instantaneous Battery I/O"] = await safe_get(_powerdata, "pbat") - inverterdata["Instantaneous Load"] = await safe_get(_powerdata, "pload") - inverterdata["Instantaneous Generation"] = await safe_get(_powerdata, "ppv") - inverterdata["Instantaneous PPV1"] = await safe_get(_pvpowerdetails, "ppv1") - inverterdata["Instantaneous PPV2"] = await safe_get(_pvpowerdetails, "ppv2") - inverterdata["Instantaneous PPV3"] = await safe_get(_pvpowerdetails, "ppv3") - inverterdata["Instantaneous PPV4"] = await safe_get(_pvpowerdetails, "ppv4") - inverterdata["pmeterDc"] = await safe_get(_pvpowerdetails, "pmeterDc") - inverterdata["pev"] = await safe_get(_powerdata, "pev") - inverterdata["Electric Vehicle Power One"] = await safe_get(_getEVdetails, "ev1Power") - inverterdata["Electric Vehicle Power Two"] = await safe_get(_getEVdetails, "ev2Power") - inverterdata["Electric Vehicle Power Three"] = await safe_get(_getEVdetails, "ev3Power") - inverterdata["Electric Vehicle Power Four"] = await safe_get(_getEVdetails, "ev4Power") - inverterdata["Instantaneous Grid I/O Total"] = await safe_get(_powerdata, "pgrid") - inverterdata["Instantaneous Grid I/O L1"] = await safe_get(_gridpowerdetails, "pmeterL1") - inverterdata["Instantaneous Grid I/O L2"] = await safe_get(_gridpowerdetails, "pmeterL2") - inverterdata["Instantaneous Grid I/O L3"] = await safe_get(_gridpowerdetails, "pmeterL3") - inverterdata["PrealL1"] = await safe_get(_powerdata, "prealL1") - inverterdata["PrealL2"] = await safe_get(_powerdata, "prealL1") - inverterdata["PrealL3"] = await safe_get(_powerdata, "prealL1") - - # Get Charge Config - _charge_config = invertor.get("ChargeConfig", {}) - - inverterdata["gridCharge"] = await safe_get(_charge_config, "gridCharge") - inverterdata["charge_timeChaf1"] = await safe_get(_charge_config, "timeChaf1") - inverterdata["charge_timeChae1"] = await safe_get(_charge_config, "timeChae1") - inverterdata["charge_timeChaf2"] = await safe_get(_charge_config, "timeChaf2") - inverterdata["charge_timeChae2"] = await safe_get(_charge_config, "timeChae2") - inverterdata["batHighCap"] = await safe_get(_charge_config, "batHighCap") - - # Get Discharge Config - _discharge_config = invertor.get("DisChargeConfig", {}) - - inverterdata["ctrDis"] = await safe_get(_discharge_config, "ctrDis") - inverterdata["discharge_timeDisf1"] = await safe_get(_discharge_config, "timeDisf1") - inverterdata["discharge_timeDise1"] = await safe_get(_discharge_config, "timeDise1") - inverterdata["discharge_timeDisf2"] = await safe_get(_discharge_config, "timeDisf2") - inverterdata["discharge_timeDise2"] = await safe_get(_discharge_config, "timeDise2") - inverterdata["batUseCap"] = await safe_get(_discharge_config, "batUseCap") - - self.data.update({invertor["sysSn"]: inverterdata}) + throttle_factor = THROTTLE_MULTIPLIER * self.LOCAL_INVERTER_COUNT + jsondata = await self.api.getdata(True, True, throttle_factor) + if jsondata is None: return self.data - except (aiohttp.client_exceptions.ClientConnectorError, aiohttp.ClientResponseError) as error: + + for invertor in jsondata: + serial = invertor.get("sysSn") + if not serial: + continue + + # Parse all data sections + inverter_data = await self._parse_inverter_data(invertor) + self.data[serial] = inverter_data + + return self.data + + except (aiohttp.ClientConnectorError, aiohttp.ClientResponseError) as error: _LOGGER.error(f"Error fetching data: {error}") self.data = None return self.data + + async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: + """Parse all data for a single inverter.""" + # Start with basic info + data = await self.parser.parse_basic_info(invertor) + + # Add EV data if available + ev_data = invertor.get("EVData", {}) + if ev_data: + data.update(await self.parser.parse_ev_data(ev_data, invertor)) + + # Add summary data + sum_data = invertor.get("SumData", {}) + if sum_data: + data.update(await self.parser.parse_summary_data(sum_data)) + + # Add energy data + energy_data = invertor.get("OneDateEnergy", {}) + if energy_data: + data.update(await self.parser.parse_energy_data(energy_data)) + + # Add power data + power_data = invertor.get("LastPower", {}) + if power_data: + one_day_power = invertor.get("OneDayPower", {}) + data.update(await self.parser.parse_power_data(power_data, one_day_power)) + + # Add configuration data + charge_config = invertor.get("ChargeConfig", {}) + if charge_config: + data.update(await self.parser.parse_charge_config(charge_config)) + + discharge_config = invertor.get("DisChargeConfig", {}) + if discharge_config: + data.update(await self.parser.parse_discharge_config(discharge_config)) + + return data \ No newline at end of file From ad6b75e3a80e2cbf610221eb0685ce88e0bb3626 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 20:34:13 +0930 Subject: [PATCH 04/19] Add localization Support (currently only english) --- custom_components/alphaess/const.py | 17 +++--- custom_components/alphaess/sensor.py | 47 ++++++++++------ .../alphaess/translations/en.json | 55 ++++++++++++------- 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index 5716d74..506b261 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -39,17 +39,16 @@ ------------------------------------------------------------------- """ -ev_charger_states = { - 1: "Available (not plugged in)", - 2: "Preparing (plugged in and not activated)", - 3: "Charging (charging with power output)", - 4: "SuspendedEVSE (already started but no available power)", - 5: "SuspendedEV (waiting for the car to respond)", - 6: "Finishing (actively stopping charging)", - 9: "Faulted (pile failure)" +EV_CHARGER_STATE_KEYS = { + 1: "available", + 2: "preparing", + 3: "charging", + 4: "suspended_evse", + 5: "suspended_ev", + 6: "finishing", + 9: "faulted" } - def increment_inverter_count(): global INVERTER_COUNT INVERTER_COUNT += 1 diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index d539a2d..d707d77 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -6,6 +6,7 @@ SensorEntity ) from homeassistant.const import CURRENCY_DOLLAR +from homeassistant.helpers.typing import StateType from .enums import AlphaESSNames from .sensorlist import FULL_SENSOR_DESCRIPTIONS, LIMITED_SENSOR_DESCRIPTIONS, EV_CHARGING_DETAILS @@ -13,7 +14,7 @@ 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_states +from .const import DOMAIN, LIMITED_INVERTER_SENSOR_LIST, EV_CHARGER_STATE_KEYS from .coordinator import AlphaESSDataUpdateCoordinator _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -132,27 +133,21 @@ def name(self): return f"{self._name}" @property - def native_value(self): - """Return the state of the resources.""" - keys = { - AlphaESSNames.DischargeTime1, - AlphaESSNames.ChargeTime1, - AlphaESSNames.DischargeTime2, - AlphaESSNames.DischargeTime2, - AlphaESSNames.ChargeTime2 - } - - if self._key in keys: - time_value = str(self._name.split()[-1]) - return self.get_time(self._name, time_value) + def native_value(self) -> StateType: + """Return the value of the sensor.""" + if self._coordinator.data is None: + return None if self._key == AlphaESSNames.evchargerstatus: - return ev_charger_states.get(self._coordinator.data[self._serial][self._name], "Unknown state") + raw_state = self._coordinator.data.get(self._serial, {}).get(self._name) + + if raw_state is None: + return None - if self._key == AlphaESSNames.ChargeRange: - return self.get_charge() + return EV_CHARGER_STATE_KEYS.get(raw_state, "unknown") - return self._coordinator.data[self._serial][self._name] + # Normal sensor handling + return self._coordinator.data.get(self._serial, {}).get(self._name) @property def native_unit_of_measurement(self): @@ -164,6 +159,22 @@ def device_class(self): """Return the device_class of the sensor.""" return self._device_class + @property + def options(self) -> list[str] | None: + """Return the list of possible options for enum sensors.""" + if self._key == AlphaESSNames.evchargerstatus: + return ["available", "preparing", "charging", "suspended_evse", + "suspended_ev", "finishing", "faulted", "unknown"] + + return None + + @property + def translation_key(self) -> str | None: + """Return the translation key.""" + if self._key == AlphaESSNames.evchargerstatus: + return "ev_charger_status" + return None + @property def state_class(self): """Return the state_class of the sensor.""" diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index 35fc9aa..7490c09 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -1,22 +1,39 @@ -{ - "title": "Alpha ESS", - "config": { - "step": { - "user": { - "description": "Enter your AppID amd AppSecret from the AlphaESS OpenAPI developer portal \n \n If you have any issues with the OpenAPI, read a list of potential fixes [here](https://github.com/CharlesGillanders/homeassistant-alphaESS?tab=readme-ov-file#issues-with-registering-systems-to-the-alphaess-openapi)", - "data": { - "AppID": "AppID", - "AppSecret": "AppSecret" - } - } - }, - "abort": { - "already_configured": "Account is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" +{ + "title": "Alpha ESS", + "config": { + "step": { + "user": { + "description": "Enter your AppID amd AppSecret from the AlphaESS OpenAPI developer portal \n \n If you have any issues with the OpenAPI, read a list of potential fixes [here](https://github.com/CharlesGillanders/homeassistant-alphaESS?tab=readme-ov-file#issues-with-registering-systems-to-the-alphaess-openapi)", + "data": { + "AppID": "AppID", + "AppSecret": "AppSecret" } + } + }, + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" } + }, + "entity": { + "sensor": { + "ev_charger_status": { + "name": "EV Charger Status", + "state": { + "available": "Available (not plugged in)", + "preparing": "Preparing (plugged in)", + "charging": "Charging", + "suspended_evse": "Suspended by EVSE", + "suspended_ev": "Suspended by EV", + "finishing": "Finishing", + "faulted": "Faulted", + "unknown": "Unknown" + } + } + } + } } From 9c17379ac06346bb67f2faf0235f6de959bb447a Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 20:34:33 +0930 Subject: [PATCH 05/19] Add LTS stats to Pv nominal Power and Inverter nominal Power --- custom_components/alphaess/sensorlist.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index a2e372c..b130a19 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -242,14 +242,17 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, "Inverter nominal Power", "mdi:lightning-bolt", UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY + SensorDeviceClass.ENERGY, + SensorStateClass.MEASUREMENT + ), _create_diagnostic_sensor( AlphaESSNames.popv, "Pv nominal Power", "mdi:lightning-bolt", UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY + SensorDeviceClass.ENERGY, + SensorStateClass.MEASUREMENT ), # Environmental impact From a0d149cb02bcf859ea512b91c7500c89fc269fd2 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 20:46:29 +0930 Subject: [PATCH 06/19] Update Sensorlist --- custom_components/alphaess/sensorlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index b130a19..0ad0549 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -243,7 +243,7 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, "mdi:lightning-bolt", UnitOfEnergy.KILO_WATT_HOUR, SensorDeviceClass.ENERGY, - SensorStateClass.MEASUREMENT + SensorStateClass.TOTAL ), _create_diagnostic_sensor( @@ -252,7 +252,7 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, "mdi:lightning-bolt", UnitOfEnergy.KILO_WATT_HOUR, SensorDeviceClass.ENERGY, - SensorStateClass.MEASUREMENT + SensorStateClass.TOTAL ), # Environmental impact From 0c6f6d6e3e825c3fbfb488981a078bd8b8fb3e2c Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sat, 28 Jun 2025 23:52:12 +0930 Subject: [PATCH 07/19] add initial WIP --- custom_components/alphaess/__init__.py | 6 ++- custom_components/alphaess/config_flow.py | 51 +++++++++++++++---- custom_components/alphaess/coordinator.py | 28 ++++++++-- custom_components/alphaess/enums.py | 5 ++ custom_components/alphaess/sensor.py | 18 ++++++- custom_components/alphaess/sensorlist.py | 41 ++++++++++++++- .../alphaess/translations/en.json | 12 ++++- 7 files changed, 142 insertions(+), 19 deletions(-) diff --git a/custom_components/alphaess/__init__.py b/custom_components/alphaess/__init__.py index 3b61758..cbe947e 100644 --- a/custom_components/alphaess/__init__.py +++ b/custom_components/alphaess/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol -from alphaess import alphaess +from .alphaesstesting import alphaess from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -43,7 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Alpha ESS from a config entry.""" - client = alphaess.alphaess(entry.data["AppID"], entry.data["AppSecret"]) + ip_address = entry.options.get("IPAddress", entry.data.get("IPAddress")) + + client = alphaess(entry.data["AppID"], entry.data["AppSecret"], ipaddress=ip_address) ESSList = await client.getESSList() for unit in ESSList: diff --git a/custom_components/alphaess/config_flow.py b/custom_components/alphaess/config_flow.py index 92c201d..129452d 100644 --- a/custom_components/alphaess/config_flow.py +++ b/custom_components/alphaess/config_flow.py @@ -5,26 +5,27 @@ from typing import Any import aiohttp -from alphaess import alphaess +from .alphaesstesting import alphaess import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN, add_inverter_to_list, increment_inverter_count - -STEP_USER_DATA_SCHEMA = vol.Schema( - {vol.Required("AppID", description={"AppID"}): str, vol.Required("AppSecret", description={"AppSecret"}): str} -) +STEP_USER_DATA_SCHEMA = vol.Schema({ + vol.Required("AppID", description="AppID"): str, + vol.Required("AppSecret", description="AppSecret"): str, + vol.Optional("IPAddress", default=None): str +}) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" - client = alphaess.alphaess(data["AppID"], data["AppSecret"]) + client = alphaess(data["AppID"], data["AppSecret"], ipaddress=data["IPAddress"]) try: await client.authenticate() @@ -56,7 +57,7 @@ class AlphaESSConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 async def async_step_user( - self, user_input: dict[str, Any] | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" @@ -73,13 +74,20 @@ async def async_step_user( errors["base"] = "invalid_auth" return self.async_create_entry( - title=user_input["AppID"], data=user_input + title=user_input["AppID"], data=user_input ) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> AlphaESSOptionsFlowHandler: + return AlphaESSOptionsFlowHandler(config_entry) + class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" @@ -87,3 +95,28 @@ class InvalidAuth(HomeAssistantError): class CannotConnect(HomeAssistantError): """Error to indicate there is a problem connecting.""" + + +class AlphaESSOptionsFlowHandler(config_entries.OptionsFlow): + """AlphaESS options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + self._config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.ConfigFlowResult: + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + schema = { + vol.Optional( + "IPAddress", + default=self._config_entry.options.get( + "IPAddress", + self._config_entry.data.get("IPAddress", ""), + ), + ): str + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(schema)) diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index b67693c..c57c5ae 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Union import aiohttp -from alphaess import alphaess +from .alphaesstesting import alphaess from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -223,7 +223,7 @@ async def parse_discharge_config(self, config: Dict) -> Dict[str, Any]: class AlphaESSDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" - def __init__(self, hass: HomeAssistant, client: alphaess.alphaess) -> None: + def __init__(self, hass: HomeAssistant, client: alphaess) -> None: """Initialize coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) self.api = client @@ -318,7 +318,6 @@ async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: if jsondata is None: return self.data - for invertor in jsondata: serial = invertor.get("sysSn") if not serial: @@ -328,6 +327,11 @@ async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: inverter_data = await self._parse_inverter_data(invertor) self.data[serial] = inverter_data + local_ip_data = next((item for item in jsondata if item.get("type") == "local_ip_data"), None) + + if local_ip_data: + self.data["local_ip_info"] = await self._parse_local_ip_data(local_ip_data) + return self.data except (aiohttp.ClientConnectorError, aiohttp.ClientResponseError) as error: @@ -335,6 +339,22 @@ async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: self.data = None return self.data + @staticmethod + async def _parse_local_ip_data(local_ip_data: dict) -> dict: + """Parse the local_ip_data dictionary and return relevant info.""" + data = {} + + device_info = local_ip_data.get("device_info", {}) + status = local_ip_data.get("status", {}) + + data["Local IP"] = local_ip_data.get("ip") + data["Cloud Connection Status"] = status.get("serverstatus") + data["Software Version"] = device_info.get("sw") + data["Serial Number"] = device_info.get("sn") + data["Hardware Version"] = device_info.get("hw") + + return data + async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: """Parse all data for a single inverter.""" # Start with basic info @@ -370,4 +390,4 @@ async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: if discharge_config: data.update(await self.parser.parse_discharge_config(discharge_config)) - return data \ No newline at end of file + return data diff --git a/custom_components/alphaess/enums.py b/custom_components/alphaess/enums.py index d63b4d2..c67ad8d 100644 --- a/custom_components/alphaess/enums.py +++ b/custom_components/alphaess/enums.py @@ -72,3 +72,8 @@ class AlphaESSNames(str, Enum): stopcharging = "Stop Charging" treePlanted = "Trees Planted" carbonReduction = "Co2 Reduction" + softwareVersion = "Software Version" + serialNumber = "Serial Number" + localIP = "Local IP" + hardwareVersion = "Hardware Version" + cloudConnectionStaus = "Cloud Connection Status" diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index d707d77..7e5b223 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -9,7 +9,8 @@ from homeassistant.helpers.typing import StateType from .enums import AlphaESSNames -from .sensorlist import FULL_SENSOR_DESCRIPTIONS, LIMITED_SENSOR_DESCRIPTIONS, EV_CHARGING_DETAILS +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 @@ -47,6 +48,8 @@ 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 + # This is done due to the limited data that inverters like the Storion-S5 support if model in LIMITED_INVERTER_SENSOR_LIST: for description in limited_key_supported_states: @@ -75,6 +78,19 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: ) ) + if has_local_ip_data: + _LOGGER.info(f"New local IP system sensor for {serial}") + for description in LOCAL_IP_SYSTEM_SENSORS: + entities.append( + AlphaESSSensor( + coordinator, + entry, + serial, + ev_charging_supported_states[description.key], + currency + ) + ) + async_add_entities(entities) return diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 0ad0549..c4b16d8 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -5,8 +5,7 @@ Sensors are organized by category and access level (full vs limited API access). """ -from typing import List, Dict, Set -from dataclasses import dataclass +from typing import List from homeassistant.components.sensor import ( SensorDeviceClass, @@ -193,6 +192,44 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, ), ] +# ============================================================================ +# LOCAL IP SENSORS +# ============================================================================ + +LOCAL_IP_SYSTEM_SENSORS = [ + AlphaESSSensorDescription( + key=AlphaESSNames.softwareVersion, + name="software Version", + icon="mdi:diversify", + device_class=SensorDeviceClass.ENUM, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.localIP, + name="IP Address", + icon="mdi:ip-network", + device_class=SensorDeviceClass.ENUM, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.hardwareVersion, + name="Hardware Version", + icon="mdi:wrench", + device_class=SensorDeviceClass.ENUM, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.cloudConnectionStaus, + name="Cloud Connection Status", + icon="mdi:cloud-cog", + native_unit_of_measurement=None, + state_class=None, + ), +] + # ============================================================================ # SYSTEM STATUS & PERFORMANCE SENSORS # ============================================================================ diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index 7490c09..4a67254 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -6,7 +6,8 @@ "description": "Enter your AppID amd AppSecret from the AlphaESS OpenAPI developer portal \n \n If you have any issues with the OpenAPI, read a list of potential fixes [here](https://github.com/CharlesGillanders/homeassistant-alphaESS?tab=readme-ov-file#issues-with-registering-systems-to-the-alphaess-openapi)", "data": { "AppID": "AppID", - "AppSecret": "AppSecret" + "AppSecret": "AppSecret", + "IPAddress": "IP Address (optional, for local API access)" } } }, @@ -19,6 +20,15 @@ "unknown": "Unexpected error" } }, + "options": { + "step": { + "init": { + "data": { + "IPAddress": "IP Address (for local API access)" + } + } + } + }, "entity": { "sensor": { "ev_charger_status": { From ef1b24c88773c54df0ce94c6e71144379dc247c0 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 29 Jun 2025 15:19:34 +0930 Subject: [PATCH 08/19] Rework coordinator and sensor list to work the old way --- custom_components/alphaess/coordinator.py | 127 ++- custom_components/alphaess/enums.py | 17 +- custom_components/alphaess/sensor.py | 9 +- custom_components/alphaess/sensorlist.py | 1055 +++++++++++++-------- 4 files changed, 794 insertions(+), 414 deletions(-) diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index c57c5ae..58b0477 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -112,6 +112,35 @@ async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[s "Household current setup": await self.dp.safe_get(ev_current, "currentsetting"), } + async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: + """Parse local IP system data.""" + if not local_ip_data: + return {} + + status = local_ip_data.get("status", {}) + device_info = local_ip_data.get("device_info", {}) + + return { + "Local IP": local_ip_data.get("ip"), + "Device Status": await self.dp.safe_get(status, "devstatus"), + "Server Status": await self.dp.safe_get(status, "serverstatus"), + "WiFi Status": await self.dp.safe_get(status, "wifistatus"), + "Connected SSID": await self.dp.safe_get(status, "connssid"), + "WiFi DHCP": await self.dp.safe_get(status, "wifidhcp"), + "WiFi IP": await self.dp.safe_get(status, "wifiip"), + "WiFi Mask": await self.dp.safe_get(status, "wifimask"), + "WiFi Gateway": await self.dp.safe_get(status, "wifigateway"), + "Serial Number": await self.dp.safe_get(device_info, "sn"), + "Device Key": await self.dp.safe_get(device_info, "key"), + "Hardware Version": await self.dp.safe_get(device_info, "hw"), + "Software Version": await self.dp.safe_get(device_info, "sw"), + "APN": await self.dp.safe_get(device_info, "apn"), + "Username": await self.dp.safe_get(device_info, "username"), + "Password": await self.dp.safe_get(device_info, "password"), + "Ethernet Module": await self.dp.safe_get(device_info, "ethmoudle"), + "4G Module": await self.dp.safe_get(device_info, "g4moudle"), + } + async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: """Parse summary statistics.""" data = { @@ -123,11 +152,12 @@ async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: "Currency": await self.dp.safe_get(sum_data, "moneyType"), } - # Convert percentages - for key in ["eselfConsumption", "eselfSufficiency"]: - value = await self.dp.safe_get(sum_data, key) - readable_key = key.replace("e", "").replace("selfC", "Self C").replace("selfS", "Self S") - data[readable_key] = value * 100 if value is not None else None + # 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") + + data["Self Consumption"] = self_consumption * 100 if self_consumption is not None else None + data["Self Sufficiency"] = self_sufficiency * 100 if self_sufficiency is not None else None return data @@ -199,10 +229,28 @@ async def parse_charge_config(self, config: Dict) -> Dict[str, Any]: for key in ["gridCharge", "batHighCap"]: data[key] = await self.dp.safe_get(config, key) - # Parse time slots - for slot in [1, 2]: - data[f"charge_timeChaf{slot}"] = await self.dp.safe_get(config, f"timeChaf{slot}") - data[f"charge_timeChae{slot}"] = await self.dp.safe_get(config, f"timeChae{slot}") + # Parse time slots with the correct key names + time_start_1 = await self.dp.safe_get(config, "timeChaf1") + time_end_1 = await self.dp.safe_get(config, "timeChae1") + time_start_2 = await self.dp.safe_get(config, "timeChaf2") + time_end_2 = await self.dp.safe_get(config, "timeChae2") + + # Format as "HH:MM - HH:MM" to match expected format + if time_start_1 and time_end_1: + data["Charge Time 1"] = f"{time_start_1} - {time_end_1}" + else: + data["Charge Time 1"] = "00:00 - 00:00" + + if time_start_2 and time_end_2: + data["Charge Time 2"] = f"{time_start_2} - {time_end_2}" + else: + data["Charge Time 2"] = "00:00 - 00:00" + + # Also keep the raw values for compatibility + data["charge_timeChaf1"] = time_start_1 + data["charge_timeChae1"] = time_end_1 + data["charge_timeChaf2"] = time_start_2 + data["charge_timeChae2"] = time_end_2 return data @@ -212,10 +260,28 @@ async def parse_discharge_config(self, config: Dict) -> Dict[str, Any]: for key in ["ctrDis", "batUseCap"]: data[key] = await self.dp.safe_get(config, key) - # Parse time slots - for slot in [1, 2]: - data[f"discharge_timeDisf{slot}"] = await self.dp.safe_get(config, f"timeDisf{slot}") - data[f"discharge_timeDise{slot}"] = await self.dp.safe_get(config, f"timeDise{slot}") + # Parse time slots with the correct key names + time_start_1 = await self.dp.safe_get(config, "timeDisf1") + time_end_1 = await self.dp.safe_get(config, "timeDise1") + time_start_2 = await self.dp.safe_get(config, "timeDisf2") + time_end_2 = await self.dp.safe_get(config, "timeDise2") + + # Format as "HH:MM - HH:MM" to match expected format + if time_start_1 and time_end_1: + data["Discharge Time 1"] = f"{time_start_1} - {time_end_1}" + else: + data["Discharge Time 1"] = "00:00 - 00:00" + + if time_start_2 and time_end_2: + data["Discharge Time 2"] = f"{time_start_2} - {time_end_2}" + else: + data["Discharge Time 2"] = "00:00 - 00:00" + + # Also keep the raw values for compatibility + data["discharge_timeDisf1"] = time_start_1 + data["discharge_timeDise1"] = time_end_1 + data["discharge_timeDisf2"] = time_start_2 + data["discharge_timeDise2"] = time_end_2 return data @@ -318,6 +384,7 @@ async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: if jsondata is None: return self.data + for invertor in jsondata: serial = invertor.get("sysSn") if not serial: @@ -327,11 +394,6 @@ async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: inverter_data = await self._parse_inverter_data(invertor) self.data[serial] = inverter_data - local_ip_data = next((item for item in jsondata if item.get("type") == "local_ip_data"), None) - - if local_ip_data: - self.data["local_ip_info"] = await self._parse_local_ip_data(local_ip_data) - return self.data except (aiohttp.ClientConnectorError, aiohttp.ClientResponseError) as error: @@ -339,27 +401,16 @@ async def _async_update_data(self) -> Optional[Dict[str, Dict[str, Any]]]: self.data = None return self.data - @staticmethod - async def _parse_local_ip_data(local_ip_data: dict) -> dict: - """Parse the local_ip_data dictionary and return relevant info.""" - data = {} - - device_info = local_ip_data.get("device_info", {}) - status = local_ip_data.get("status", {}) - - data["Local IP"] = local_ip_data.get("ip") - data["Cloud Connection Status"] = status.get("serverstatus") - data["Software Version"] = device_info.get("sw") - data["Serial Number"] = device_info.get("sn") - data["Hardware Version"] = device_info.get("hw") - - return data - async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: """Parse all data for a single inverter.""" # Start with basic info data = await self.parser.parse_basic_info(invertor) + # Add LocalIPData if available + local_ip_data = invertor.get("LocalIPData", {}) + if local_ip_data: + data.update(await self.parser.parse_local_ip_data(local_ip_data)) + # Add EV data if available ev_data = invertor.get("EVData", {}) if ev_data: @@ -390,4 +441,10 @@ async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: if discharge_config: data.update(await self.parser.parse_discharge_config(discharge_config)) - return data + # Add Charging Range (combining charge and discharge data) + if charge_config or discharge_config: + bat_high_cap = charge_config.get("batHighCap", 90) if charge_config else 90 + bat_use_cap = discharge_config.get("batUseCap", 10) if discharge_config else 10 + data["Charging Range"] = f"{bat_use_cap}% - {bat_high_cap}%" + + return data \ No newline at end of file diff --git a/custom_components/alphaess/enums.py b/custom_components/alphaess/enums.py index c67ad8d..8c15e0b 100644 --- a/custom_components/alphaess/enums.py +++ b/custom_components/alphaess/enums.py @@ -76,4 +76,19 @@ class AlphaESSNames(str, Enum): serialNumber = "Serial Number" localIP = "Local IP" hardwareVersion = "Hardware Version" - cloudConnectionStaus = "Cloud Connection Status" + cloudConnectionStatus = "Cloud Connection Status" + deviceStatus = "Device Status" + serverStatus = "Server Status" + wifiStatus = "WiFi Status" + connectedSSID = "Connected SSID" + wifiDHCP = "WiFi DHCP" + wifiIP = "WiFi IP" + wifiMask = "WiFi Mask" + wifiGateway = "WiFi Gateway" + deviceSerialNumber = "Device Serial Number" + deviceKey = "Device Key" + apn = "APN" + username = "Username" + password = "Password" + ethernetModule = "Ethernet Module" + fourGModule = "4G Module" diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 7e5b223..83a23be 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -9,8 +9,7 @@ from homeassistant.helpers.typing import StateType from .enums import AlphaESSNames -from .sensorlist import FULL_SENSOR_DESCRIPTIONS, LIMITED_SENSOR_DESCRIPTIONS, EV_CHARGING_DETAILS, \ - LOCAL_IP_SYSTEM_SENSORS +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 @@ -39,6 +38,10 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: description.key: description for description in EV_CHARGING_DETAILS } + local_ip_supported_states = { + description.key: description for description in LOCAL_IP_SYSTEM_SENSORS + } + _LOGGER.info(f"Initializing Inverters") for serial, data in coordinator.data.items(): model = data.get("Model") @@ -86,7 +89,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: coordinator, entry, serial, - ev_charging_supported_states[description.key], + local_ip_supported_states[description.key], currency ) ) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index c4b16d8..5c478ae 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -1,147 +1,141 @@ -""" -AlphaESS Home Assistant Integration - Sensor Definitions - -This module defines all sensor, button, and number entity descriptions for the AlphaESS integration. -Sensors are organized by category and access level (full vs limited API access). -""" - from typing import List from homeassistant.components.sensor import ( SensorDeviceClass, SensorStateClass, ) -from homeassistant.const import ( - UnitOfEnergy, - PERCENTAGE, - UnitOfPower, - CURRENCY_DOLLAR, - EntityCategory, - UnitOfMass -) +from homeassistant.const import UnitOfEnergy, PERCENTAGE, UnitOfPower, CURRENCY_DOLLAR, EntityCategory, UnitOfMass -from .entity import ( - AlphaESSSensorDescription, - AlphaESSButtonDescription, - AlphaESSNumberDescription -) +from .entity import AlphaESSSensorDescription, AlphaESSButtonDescription, AlphaESSNumberDescription from .enums import AlphaESSNames - -# ============================================================================ -# SENSOR CATEGORIES -# ============================================================================ - -def _create_energy_sensor(key: AlphaESSNames, name: str, - increasing: bool = True) -> AlphaESSSensorDescription: - """Helper to create energy sensors (kWh, total increasing).""" - return AlphaESSSensorDescription( - key=key, - name=name, +FULL_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ + AlphaESSSensorDescription( + key=AlphaESSNames.SolarProduction, + name="Solar Production", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING if increasing else SensorStateClass.TOTAL, - ) - - -def _create_power_sensor(key: AlphaESSNames, name: str, - icon: str = "mdi:flash") -> AlphaESSSensorDescription: - """Helper to create instantaneous power sensors (W).""" - return AlphaESSSensorDescription( - key=key, - name=name, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.SolarToBattery, + name="Solar to Battery", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.SolarToGrid, + name="Solar to Grid", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.SolarToLoad, + name="Solar to Load", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.TotalLoad, + name="Total Load", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridToLoad, + name="Grid to Load", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridToBattery, + name="Grid to Battery", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Charge, + name="Charge", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Discharge, + name="Discharge", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.EVCharger, + name="EV Charger", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Generation, + name="Instantaneous Generation", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, - icon=icon, - ) - - -def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, - unit: str = None, - device_class: str = None, - state_class: SensorStateClass = None) -> AlphaESSSensorDescription: - """Helper to create diagnostic sensors.""" - return AlphaESSSensorDescription( - key=key, - name=name, - icon=icon, - native_unit_of_measurement=unit, - device_class=device_class, - state_class=state_class, - entity_category=EntityCategory.DIAGNOSTIC, - ) - - -# ============================================================================ -# ENERGY FLOW SENSORS - Track energy movement between components -# ============================================================================ - -ENERGY_FLOW_SENSORS = [ - # Solar energy distribution - _create_energy_sensor(AlphaESSNames.SolarProduction, "Solar Production"), - _create_energy_sensor(AlphaESSNames.SolarToBattery, "Solar to Battery"), - _create_energy_sensor(AlphaESSNames.SolarToGrid, "Solar to Grid"), - _create_energy_sensor(AlphaESSNames.SolarToLoad, "Solar to Load"), - - # Grid interactions - _create_energy_sensor(AlphaESSNames.GridToLoad, "Grid to Load"), - _create_energy_sensor(AlphaESSNames.GridToBattery, "Grid to Battery"), - - # Battery operations - _create_energy_sensor(AlphaESSNames.Charge, "Charge"), - _create_energy_sensor(AlphaESSNames.Discharge, "Discharge"), - - # Consumption - _create_energy_sensor(AlphaESSNames.TotalLoad, "Total Load"), - _create_energy_sensor(AlphaESSNames.EVCharger, "EV Charger"), - - # Totals - _create_energy_sensor(AlphaESSNames.Total_Generation, "Total Generation"), -] - -# ============================================================================ -# INSTANTANEOUS POWER SENSORS - Real-time power measurements -# ============================================================================ - -INSTANTANEOUS_POWER_SENSORS = [ - # Generation - _create_power_sensor(AlphaESSNames.Generation, "Instantaneous Generation"), - - # Individual PV strings (only in FULL access) - _create_power_sensor(AlphaESSNames.PPV1, "Instantaneous PPV1"), - _create_power_sensor(AlphaESSNames.PPV2, "Instantaneous PPV2"), - _create_power_sensor(AlphaESSNames.PPV3, "Instantaneous PPV3"), - _create_power_sensor(AlphaESSNames.PPV4, "Instantaneous PPV4"), - - # Grid phases (only in FULL access) - _create_power_sensor(AlphaESSNames.GridIOL1, "Instantaneous Grid I/O L1"), - _create_power_sensor(AlphaESSNames.GridIOL2, "Instantaneous Grid I/O L2"), - _create_power_sensor(AlphaESSNames.GridIOL3, "Instantaneous Grid I/O L3"), - - # Totals (available in both FULL and LIMITED) - _create_power_sensor(AlphaESSNames.GridIOTotal, "Instantaneous Grid I/O Total"), - _create_power_sensor(AlphaESSNames.Load, "Instantaneous Load"), - - # Battery - _create_power_sensor(AlphaESSNames.BatteryIO, "Instantaneous Battery I/O"), - - # DC meter - _create_power_sensor(AlphaESSNames.pmeterDc, "pmeterDc", "mdi:current-dc"), - - # Unknown purpose - _create_power_sensor(AlphaESSNames.pev, "pev"), - _create_power_sensor(AlphaESSNames.PrealL1, "PrealL1"), - _create_power_sensor(AlphaESSNames.PrealL2, "PrealL2"), - _create_power_sensor(AlphaESSNames.PrealL3, "PrealL3"), -] - -# ============================================================================ -# BATTERY SENSORS - Battery state and configuration -# ============================================================================ - -BATTERY_SENSORS = [ - # Battery state + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PPV1, + name="Instantaneous PPV1", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PPV2, + name="Instantaneous PPV2", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PPV3, + name="Instantaneous PPV3", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PPV4, + name="Instantaneous PPV4", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridIOL1, + name="Instantaneous Grid I/O L1", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridIOL2, + name="Instantaneous Grid I/O L2", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridIOL3, + name="Instantaneous Grid I/O L3", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), AlphaESSSensorDescription( key=AlphaESSNames.BatterySOC, name="Instantaneous Battery SOC", @@ -149,8 +143,246 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), + AlphaESSSensorDescription( + key=AlphaESSNames.BatteryIO, + name="Instantaneous Battery I/O", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridIOTotal, + name="Instantaneous Grid I/O Total", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Load, + name="Instantaneous Load", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Total_Generation, + name="Total Generation", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Income, + name="Total Income", + icon="mdi:cash-multiple", + native_unit_of_measurement=CURRENCY_DOLLAR, + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.SelfConsumption, + name="Self Consumption", + icon="mdi:home-percent", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.SelfSufficiency, + name="Self Sufficiency", + icon="mdi:home-percent", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.EmsStatus, + name="EMS Status", + icon="mdi:home-battery", + device_class=SensorDeviceClass.ENUM, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.usCapacity, + name="Maximum Battery Capacity", + icon="mdi:home-percent", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.cobat, + name="Installed Capacity", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.surplusCobat, + name="Current Capacity", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ChargeTime1, + name="Charge Time 1", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ChargeTime2, + name="Charge Time 2", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DischargeTime1, + name="Discharge Time 1", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.DischargeTime2, + name="Discharge Time 2", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ChargeRange, + name="Charging Range", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:battery-lock-open", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.mbat, + name="Battery Model", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:battery-heart-variant", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.poinv, + name="Inverter nominal Power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:lightning-bolt", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.popv, + name="Pv nominal Power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:lightning-bolt", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.pmeterDc, + name="pmeterDc", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:current-dc", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerOne, + name="Electric Vehicle Power One", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerTwo, + name="Electric Vehicle Power Two", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerThree, + name="Electric Vehicle Power Three", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerFour, + name="Electric Vehicle Power Four", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.pev, + name="pev", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PrealL1, + name="PrealL1", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PrealL2, + name="PrealL2", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PrealL3, + name="PrealL3", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.carbonReduction, + name="Co2 Reduction", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:molecule-co2", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.treePlanted, + name="Trees Planted", + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:tree", + ) +] - # Alternative SOC (only in LIMITED access) +LIMITED_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( key=AlphaESSNames.StateOfCharge, name="State of Charge", @@ -158,84 +390,83 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), - - # Battery capacity info - _create_diagnostic_sensor( - AlphaESSNames.usCapacity, - "Maximum Battery Capacity", - "mdi:home-percent", - PERCENTAGE, - state_class=SensorStateClass.TOTAL - ), - _create_diagnostic_sensor( - AlphaESSNames.cobat, - "Installed Capacity", - "mdi:battery-heart-variant", - UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL - ), - _create_diagnostic_sensor( - AlphaESSNames.surplusCobat, - "Current Capacity", - "mdi:battery-heart-variant", - UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL + AlphaESSSensorDescription( + key=AlphaESSNames.SolarProduction, + name="Solar Production", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), - - # Battery model - _create_diagnostic_sensor( - AlphaESSNames.mbat, - "Battery Model", - "mdi:battery-heart-variant" + AlphaESSSensorDescription( + key=AlphaESSNames.SolarToBattery, + name="Solar to Battery", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), -] - -# ============================================================================ -# LOCAL IP SENSORS -# ============================================================================ - -LOCAL_IP_SYSTEM_SENSORS = [ AlphaESSSensorDescription( - key=AlphaESSNames.softwareVersion, - name="software Version", - icon="mdi:diversify", - device_class=SensorDeviceClass.ENUM, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC + key=AlphaESSNames.TotalLoad, + name="Total Load", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( - key=AlphaESSNames.localIP, - name="IP Address", - icon="mdi:ip-network", - device_class=SensorDeviceClass.ENUM, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC + key=AlphaESSNames.GridToLoad, + name="Grid to Load", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.GridToBattery, + name="Grid to Battery", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Charge, + name="Charge", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Discharge, + name="Discharge", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.EVCharger, + name="EV Charger", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), AlphaESSSensorDescription( - key=AlphaESSNames.hardwareVersion, - name="Hardware Version", - icon="mdi:wrench", - device_class=SensorDeviceClass.ENUM, - state_class=None, - entity_category=EntityCategory.DIAGNOSTIC + key=AlphaESSNames.GridIOTotal, + name="Instantaneous Grid I/O Total", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), AlphaESSSensorDescription( - key=AlphaESSNames.cloudConnectionStaus, - name="Cloud Connection Status", - icon="mdi:cloud-cog", - native_unit_of_measurement=None, - state_class=None, + key=AlphaESSNames.Load, + name="Instantaneous Load", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.Total_Generation, + name="Total Generation", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), -] - -# ============================================================================ -# SYSTEM STATUS & PERFORMANCE SENSORS -# ============================================================================ - -SYSTEM_STATUS_SENSORS = [ - # Financial AlphaESSSensorDescription( key=AlphaESSNames.Income, name="Total Income", @@ -244,8 +475,6 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL, ), - - # Self-sufficiency metrics AlphaESSSensorDescription( key=AlphaESSNames.SelfConsumption, name="Self Consumption", @@ -262,147 +491,193 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), - - # System status AlphaESSSensorDescription( key=AlphaESSNames.EmsStatus, name="EMS Status", icon="mdi:home-battery", device_class=SensorDeviceClass.ENUM, - state_class=None, # ENUM sensors cannot have a state_class + state_class=None, entity_category=EntityCategory.DIAGNOSTIC ), - - # System specs - _create_diagnostic_sensor( - AlphaESSNames.poinv, - "Inverter nominal Power", - "mdi:lightning-bolt", - UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL - + AlphaESSSensorDescription( + key=AlphaESSNames.usCapacity, + name="Maximum Battery Capacity", + icon="mdi:home-percent", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC ), - _create_diagnostic_sensor( - AlphaESSNames.popv, - "Pv nominal Power", - "mdi:lightning-bolt", - UnitOfEnergy.KILO_WATT_HOUR, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL + AlphaESSSensorDescription( + key=AlphaESSNames.cobat, + name="Installed Capacity", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC ), - - # Environmental impact AlphaESSSensorDescription( - key=AlphaESSNames.carbonReduction, - name="Co2 Reduction", - native_unit_of_measurement=UnitOfMass.KILOGRAMS, - device_class=SensorDeviceClass.WEIGHT, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:molecule-co2", + key=AlphaESSNames.surplusCobat, + name="Current Capacity", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC ), AlphaESSSensorDescription( - key=AlphaESSNames.treePlanted, - name="Trees Planted", + key=AlphaESSNames.ChargeTime1, + name="Charge Time 1", native_unit_of_measurement=None, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:tree", - ), -] - -# ============================================================================ -# SCHEDULING SENSORS - Charge/discharge time configurations -# ============================================================================ - -SCHEDULING_SENSORS = [ - _create_diagnostic_sensor( - AlphaESSNames.ChargeTime1, "Charge Time 1", "mdi:clock-time-ten" - ), - _create_diagnostic_sensor( - AlphaESSNames.ChargeTime2, "Charge Time 2", "mdi:clock-time-ten" - ), - _create_diagnostic_sensor( - AlphaESSNames.DischargeTime1, "Discharge Time 1", "mdi:clock-time-ten" - ), - _create_diagnostic_sensor( - AlphaESSNames.DischargeTime2, "Discharge Time 2", "mdi:clock-time-ten" - ), - _create_diagnostic_sensor( - AlphaESSNames.ChargeRange, "Charging Range", "mdi:battery-lock-open" + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", ), -] - -# ============================================================================ -# EV CHARGER SENSORS -# ============================================================================ - -EV_POWER_SENSORS = [ - _create_power_sensor( - AlphaESSNames.ElectricVehiclePowerOne, - "Electric Vehicle Power One", - "mdi:car-electric" - ), - _create_power_sensor( - AlphaESSNames.ElectricVehiclePowerTwo, - "Electric Vehicle Power Two", - "mdi:car-electric" - ), - _create_power_sensor( - AlphaESSNames.ElectricVehiclePowerThree, - "Electric Vehicle Power Three", - "mdi:car-electric" - ), - _create_power_sensor( - AlphaESSNames.ElectricVehiclePowerFour, - "Electric Vehicle Power Four", - "mdi:car-electric" + AlphaESSSensorDescription( + key=AlphaESSNames.ChargeTime2, + name="Charge Time 2", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", ), -] - -EV_CHARGING_DETAILS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( - key=AlphaESSNames.evchargersn, - name="EV Charger S/N", - icon="mdi:ev-station", + key=AlphaESSNames.DischargeTime1, + name="Discharge Time 1", native_unit_of_measurement=None, state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", ), AlphaESSSensorDescription( - key=AlphaESSNames.evchargermodel, - name="EV Charger Model", - icon="mdi:ev-station", + key=AlphaESSNames.DischargeTime2, + name="Discharge Time 2", native_unit_of_measurement=None, state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:clock-time-ten", ), AlphaESSSensorDescription( - key=AlphaESSNames.evchargerstatusraw, - name="EV Charger Status Raw", - icon="mdi:ev-station", + key=AlphaESSNames.ChargeRange, + name="Charging Range", native_unit_of_measurement=None, state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:battery-lock-open", ), AlphaESSSensorDescription( - key=AlphaESSNames.evchargerstatus, - name="EV Charger Status", - icon="mdi:ev-station", - device_class="enum", + key=AlphaESSNames.mbat, + name="Battery Model", native_unit_of_measurement=None, state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:battery-heart-variant", ), AlphaESSSensorDescription( - key=AlphaESSNames.evcurrentsetting, - name="Household current setup", - icon="mdi:ev-station", - device_class=SensorDeviceClass.CURRENT, - native_unit_of_measurement="A", + key=AlphaESSNames.poinv, + name="Inverter nominal Power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=None, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:lightning-bolt", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.popv, + name="Pv nominal Power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=None, + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:lightning-bolt", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.pmeterDc, + name="pmeterDc", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:current-dc", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerOne, + name="Electric Vehicle Power One", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerTwo, + name="Electric Vehicle Power Two", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerThree, + name="Electric Vehicle Power Three", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.ElectricVehiclePowerFour, + name="Electric Vehicle Power Four", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:car-electric", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.pev, + name="pev", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PrealL1, + name="PrealL1", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PrealL2, + name="PrealL2", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.PrealL3, + name="PrealL3", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:flash", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.carbonReduction, + name="Carbon Reduction", + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + device_class=SensorDeviceClass.WEIGHT, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:molecule-co2", + ), + AlphaESSSensorDescription( + key=AlphaESSNames.treePlanted, + name="Trees Planted", + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:tree", ) ] -# ============================================================================ -# CONTROL ENTITIES - Buttons and Numbers -# ============================================================================ - SUPPORT_DISCHARGE_AND_CHARGE_BUTTON_DESCRIPTIONS: List[AlphaESSButtonDescription] = [ AlphaESSButtonDescription( key=AlphaESSNames.ButtonDischargeFifteen, @@ -455,6 +730,7 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, entity_category=EntityCategory.CONFIG, icon="mdi:battery-sync", native_unit_of_measurement=PERCENTAGE, + ), AlphaESSNumberDescription( key=AlphaESSNames.batUseCap, @@ -465,7 +741,8 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, ) ] -EV_DISCHARGE_AND_CHARGE_BUTTONS: List[AlphaESSButtonDescription] = [ +EV_DISCHARGE_AND_CHARGE_BUTTONS: List[AlphaESSNumberDescription] = [ + AlphaESSButtonDescription( key=AlphaESSNames.stopcharging, name="Stop Charging", @@ -478,79 +755,107 @@ def _create_diagnostic_sensor(key: AlphaESSNames, name: str, icon: str, icon="mdi:battery-plus", entity_category=EntityCategory.CONFIG, ) -] - -# ============================================================================ -# SENSOR COLLECTIONS - Full vs Limited API Access -# ============================================================================ -# Sensors exclusive to FULL API access -FULL_ONLY_SENSORS = [ - # Individual PV string monitoring - sensor for sensor in INSTANTANEOUS_POWER_SENSORS - if sensor.key in [ - AlphaESSNames.PPV1, AlphaESSNames.PPV2, - AlphaESSNames.PPV3, AlphaESSNames.PPV4 - ] - ] + [ - # Individual grid phase monitoring - sensor for sensor in INSTANTANEOUS_POWER_SENSORS - if sensor.key in [ - AlphaESSNames.GridIOL1, AlphaESSNames.GridIOL2, - AlphaESSNames.GridIOL3 - ] - ] + [ - # Solar to grid energy flow - _create_energy_sensor(AlphaESSNames.SolarToGrid, "Solar to Grid"), - _create_energy_sensor(AlphaESSNames.SolarToLoad, "Solar to Load"), - - # Additional sensors only in full - _create_power_sensor(AlphaESSNames.Generation, "Instantaneous Generation"), - AlphaESSSensorDescription( - key=AlphaESSNames.BatterySOC, - name="Instantaneous Battery SOC", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, - ), - _create_power_sensor(AlphaESSNames.BatteryIO, "Instantaneous Battery I/O"), - ] +] -# Sensors exclusive to LIMITED API access -LIMITED_ONLY_SENSORS = [ +LOCAL_IP_SYSTEM_SENSORS: List[AlphaESSSensorDescription] = [ AlphaESSSensorDescription( - key=AlphaESSNames.StateOfCharge, - name="State of Charge", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, + key=AlphaESSNames.localIP, + name="Local IP", + icon="mdi:ip-network", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.wifiStatus, + name="WiFi Status", + icon="mdi:wifi", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.connectedSSID, + name="Connected SSID", + icon="mdi:wifi", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.softwareVersion, + name="Software Version", + icon="mdi:package-variant", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.hardwareVersion, + name="Hardware Version", + icon="mdi:chip", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.deviceSerialNumber, + name="Device Serial Number", + icon="mdi:identifier", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.cloudConnectionStatus, + name="Cloud Connection Status", + icon="mdi:cloud-check", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, ), ] -# Common sensors available in both FULL and LIMITED -COMMON_SENSORS = ( - ENERGY_FLOW_SENSORS + - SYSTEM_STATUS_SENSORS + - SCHEDULING_SENSORS + - EV_POWER_SENSORS + - [sensor for sensor in BATTERY_SENSORS if sensor.key != AlphaESSNames.StateOfCharge] + - [sensor for sensor in INSTANTANEOUS_POWER_SENSORS - if sensor.key in [ - AlphaESSNames.GridIOTotal, AlphaESSNames.Load, - AlphaESSNames.pmeterDc, AlphaESSNames.pev, - AlphaESSNames.PrealL1, AlphaESSNames.PrealL2, AlphaESSNames.PrealL3 - ]] -) - -# Remove duplicates from COMMON_SENSORS -full_only_keys = {sensor.key for sensor in FULL_ONLY_SENSORS} -COMMON_SENSORS = [sensor for sensor in COMMON_SENSORS if sensor.key not in full_only_keys] +EV_CHARGING_DETAILS: List[AlphaESSSensorDescription] = [ + AlphaESSSensorDescription( + key=AlphaESSNames.evchargersn, + name="EV Charger S/N", + icon="mdi:ev-station", + native_unit_of_measurement=None, + state_class=None, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.evchargermodel, + name="EV Charger Model", + icon="mdi:ev-station", + native_unit_of_measurement=None, + state_class=None, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.evchargerstatusraw, + name="EV Charger Status Raw", + icon="mdi:ev-station", + native_unit_of_measurement=None, + state_class=None, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.evchargerstatus, + name="EV Charger Status", + icon="mdi:ev-station", + device_class="enum", + native_unit_of_measurement=None, + state_class=None, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.evcurrentsetting, + name="Household current setup", + icon="mdi:ev-station", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement="A", + state_class=None, + ) -# Final collections -FULL_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = ( - COMMON_SENSORS + FULL_ONLY_SENSORS -) +] -LIMITED_SENSOR_DESCRIPTIONS: List[AlphaESSSensorDescription] = ( - COMMON_SENSORS + LIMITED_ONLY_SENSORS -) From bd7544d1a1a09767569fd16d2f2fdf538f78ff44 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 29 Jun 2025 15:54:21 +0930 Subject: [PATCH 09/19] Add new device attribute information for local IP devices --- custom_components/alphaess/button.py | 21 ++++++++++++++++--- custom_components/alphaess/coordinator.py | 4 ++-- custom_components/alphaess/manifest.json | 2 +- custom_components/alphaess/number.py | 21 ++++++++++++++++--- custom_components/alphaess/sensor.py | 25 ++++++++++++++++++----- custom_components/alphaess/sensorlist.py | 1 - 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index 2a2084f..931d971 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -42,19 +42,21 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: description.key: description for description in EV_DISCHARGE_AND_CHARGE_BUTTONS } + for serial, data in coordinator.data.items(): model = data.get("Model") + has_local_ip_data = 'Local IP' in data if model not in INVERTER_SETTING_BLACKLIST: for description in full_button_supported_states: button_entities.append( - AlphaESSBatteryButton(coordinator, entry, serial, full_button_supported_states[description])) + AlphaESSBatteryButton(coordinator, entry, serial, full_button_supported_states[description], has_local_connection=has_local_ip_data)) ev_charger = data.get("EV Charger S/N") if ev_charger: for description in ev_charging_supported_states: button_entities.append( AlphaESSBatteryButton( - coordinator, entry, serial, ev_charging_supported_states[description], True + coordinator, entry, serial, ev_charging_supported_states[description], True, has_local_connection=has_local_ip_data ) ) @@ -63,7 +65,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: class AlphaESSBatteryButton(CoordinatorEntity, ButtonEntity): - def __init__(self, coordinator, config, serial, key_supported_states, ev_charger=False): + def __init__(self, coordinator, config, serial, key_supported_states, ev_charger=False, has_local_connection=False): super().__init__(coordinator) self._serial = serial self._coordinator = coordinator @@ -92,6 +94,19 @@ def __init__(self, coordinator, config, serial, key_supported_states, ev_charger model_id=coordinator.data[invertor]["EV Charger S/N"], name=f"Alpha ESS Charger : {coordinator.data[invertor]["EV Charger S/N"]}", ) + elif "Local IP" in coordinator.data[invertor]: + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, serial)}, + serial_number=coordinator.data[invertor]["Device Serial Number"], + sw_version=coordinator.data[invertor]["Software Version"], + hw_version=coordinator.data[invertor]["Hardware Version"], + manufacturer="AlphaESS", + model=coordinator.data[invertor]["Model"], + model_id=self._serial, + name=f"Alpha ESS Energy Statistics LOCAL : {serial}", + configuration_url=f"http://{coordinator.data[invertor]["Local IP"]}" + ) elif self._serial == serial: self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 58b0477..4189cb7 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -123,14 +123,14 @@ async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: return { "Local IP": local_ip_data.get("ip"), "Device Status": await self.dp.safe_get(status, "devstatus"), - "Server Status": await self.dp.safe_get(status, "serverstatus"), + "Cloud Connection Status": await self.dp.safe_get(status, "serverstatus"), "WiFi Status": await self.dp.safe_get(status, "wifistatus"), "Connected SSID": await self.dp.safe_get(status, "connssid"), "WiFi DHCP": await self.dp.safe_get(status, "wifidhcp"), "WiFi IP": await self.dp.safe_get(status, "wifiip"), "WiFi Mask": await self.dp.safe_get(status, "wifimask"), "WiFi Gateway": await self.dp.safe_get(status, "wifigateway"), - "Serial Number": await self.dp.safe_get(device_info, "sn"), + "Device Serial Number": await self.dp.safe_get(device_info, "sn"), "Device Key": await self.dp.safe_get(device_info, "key"), "Hardware Version": await self.dp.safe_get(device_info, "hw"), "Software Version": await self.dp.safe_get(device_info, "sw"), diff --git a/custom_components/alphaess/manifest.json b/custom_components/alphaess/manifest.json index 6e118f8..0ec8438 100644 --- a/custom_components/alphaess/manifest.json +++ b/custom_components/alphaess/manifest.json @@ -15,7 +15,7 @@ "requirements": [ "alphaessopenapi==0.0.13" ], - "version": "0.6.1" + "version": "0.7.0" } diff --git a/custom_components/alphaess/number.py b/custom_components/alphaess/number.py index 99a8591..b885ef9 100644 --- a/custom_components/alphaess/number.py +++ b/custom_components/alphaess/number.py @@ -23,10 +23,11 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: for serial, data in coordinator.data.items(): model = data.get("Model") + has_local_ip_data = 'Local IP' in data if model not in INVERTER_SETTING_BLACKLIST: for description in full_number_supported_states: number_entities.append( - AlphaNumber(coordinator, serial, entry, full_number_supported_states[description])) + AlphaNumber(coordinator, serial, entry, full_number_supported_states[description], has_local_connection=has_local_ip_data)) async_add_entities(number_entities) @@ -34,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: class AlphaNumber(CoordinatorEntity, RestoreNumber): """Battery use capacity number entity.""" - def __init__(self, coordinator, serial, config, full_number_supported_states): + def __init__(self, coordinator, serial, config, full_number_supported_states, has_local_connection=False): super().__init__(coordinator) self._coordinator = coordinator self._serial = serial @@ -52,7 +53,21 @@ def __init__(self, coordinator, serial, config, full_number_supported_states): for invertor in coordinator.data: serial = invertor.upper() - if self._serial == serial: + if "Local IP" in coordinator.data[invertor]: + _LOGGER.info(f"INVERTER LOCAL DATA = {coordinator.data[invertor]}") + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, serial)}, + serial_number=coordinator.data[invertor]["Device Serial Number"], + sw_version=coordinator.data[invertor]["Software Version"], + hw_version=coordinator.data[invertor]["Hardware Version"], + manufacturer="AlphaESS", + model=coordinator.data[invertor]["Model"], + model_id=self._serial, + name=f"Alpha ESS Energy Statistics LOCAL : {serial}", + configuration_url=f"http://{coordinator.data[invertor]["Local IP"]}" + ) + elif self._serial == serial: self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, serial)}, diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 83a23be..5bc0e76 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -58,14 +58,14 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: for description in limited_key_supported_states: entities.append( AlphaESSSensor( - coordinator, entry, serial, limited_key_supported_states[description], currency + coordinator, entry, serial, limited_key_supported_states[description], currency, has_local_connection=has_local_ip_data ) ) else: for description in full_key_supported_states: entities.append( AlphaESSSensor( - coordinator, entry, serial, full_key_supported_states[description], currency + coordinator, entry, serial, full_key_supported_states[description], currency, has_local_connection=has_local_ip_data ) ) @@ -77,7 +77,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: for description in EV_CHARGING_DETAILS: entities.append( AlphaESSSensor( - coordinator, entry, serial, ev_charging_supported_states[description.key], currency, True + coordinator, entry, serial, ev_charging_supported_states[description.key], currency, True, has_local_connection=has_local_ip_data ) ) @@ -90,7 +90,8 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: entry, serial, local_ip_supported_states[description.key], - currency + currency, + has_local_connection=has_local_ip_data ) ) @@ -102,7 +103,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: class AlphaESSSensor(CoordinatorEntity, SensorEntity): """Alpha ESS Base Sensor.""" - def __init__(self, coordinator, config, serial, key_supported_states, currency, ev_charger=False): + def __init__(self, coordinator, config, serial, key_supported_states, currency, ev_charger=False, has_local_connection=False): """Initialize the sensor.""" super().__init__(coordinator) self._config = config @@ -131,6 +132,19 @@ def __init__(self, coordinator, config, serial, key_supported_states, currency, model_id=coordinator.data[invertor]["EV Charger S/N"], name=f"Alpha ESS Charger : {coordinator.data[invertor]["EV Charger S/N"]}", ) + elif "Local IP" in coordinator.data[invertor]: + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, serial)}, + serial_number=coordinator.data[invertor]["Device Serial Number"], + sw_version=coordinator.data[invertor]["Software Version"], + hw_version=coordinator.data[invertor]["Hardware Version"], + manufacturer="AlphaESS", + model=coordinator.data[invertor]["Model"], + model_id=self._serial, + name=f"Alpha ESS Energy Statistics LOCAL : {serial}", + configuration_url=f"http://{coordinator.data[invertor]["Local IP"]}" + ) elif self._serial == serial: self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -141,6 +155,7 @@ def __init__(self, coordinator, config, serial, key_supported_states, currency, name=f"Alpha ESS Energy Statistics : {serial}", ) + @property def unique_id(self): """Return a unique ID to use for this entity.""" diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 5c478ae..abf021a 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -814,7 +814,6 @@ native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, - device_class=SensorDeviceClass.ENUM, ), ] From de0444c0f10598e3ddb2bf883c301708bd86ed10 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 29 Jun 2025 16:20:11 +0930 Subject: [PATCH 10/19] Add comment for how to remove --- custom_components/alphaess/config_flow.py | 2 +- custom_components/alphaess/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/alphaess/config_flow.py b/custom_components/alphaess/config_flow.py index 129452d..a87ea3a 100644 --- a/custom_components/alphaess/config_flow.py +++ b/custom_components/alphaess/config_flow.py @@ -18,7 +18,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema({ vol.Required("AppID", description="AppID"): str, vol.Required("AppSecret", description="AppSecret"): str, - vol.Optional("IPAddress", default=None): str + vol.Optional("IPAddress", default=None): vol.Any(None, str) }) diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index 4a67254..2fbbc65 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -3,7 +3,7 @@ "config": { "step": { "user": { - "description": "Enter your AppID amd AppSecret from the AlphaESS OpenAPI developer portal \n \n If you have any issues with the OpenAPI, read a list of potential fixes [here](https://github.com/CharlesGillanders/homeassistant-alphaESS?tab=readme-ov-file#issues-with-registering-systems-to-the-alphaess-openapi)", + "description": "Enter your AppID and AppSecret from the AlphaESS OpenAPI developer portal \n \n If you have any issues with the OpenAPI, read a list of potential fixes [here](https://github.com/CharlesGillanders/homeassistant-alphaESS?tab=readme-ov-file#issues-with-registering-systems-to-the-alphaess-openapi)", "data": { "AppID": "AppID", "AppSecret": "AppSecret", @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "IPAddress": "IP Address (for local API access)" + "IPAddress": "IP Address (local API access) 0 to disable" } } } From 8ff2f7ab2d48d38d016a97e22f9c0918b314006e Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Sun, 29 Jun 2025 17:07:23 +0930 Subject: [PATCH 11/19] Move to using alphaessopenapi package 0.0.14 --- custom_components/alphaess/__init__.py | 4 ++-- custom_components/alphaess/config_flow.py | 4 ++-- custom_components/alphaess/coordinator.py | 4 ++-- custom_components/alphaess/manifest.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/alphaess/__init__.py b/custom_components/alphaess/__init__.py index cbe947e..5aae310 100644 --- a/custom_components/alphaess/__init__.py +++ b/custom_components/alphaess/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol -from .alphaesstesting import alphaess +from alphaess import alphaess from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ip_address = entry.options.get("IPAddress", entry.data.get("IPAddress")) - client = alphaess(entry.data["AppID"], entry.data["AppSecret"], ipaddress=ip_address) + client = alphaess.alphaess(entry.data["AppID"], entry.data["AppSecret"], ipaddress=ip_address) ESSList = await client.getESSList() for unit in ESSList: diff --git a/custom_components/alphaess/config_flow.py b/custom_components/alphaess/config_flow.py index a87ea3a..532095a 100644 --- a/custom_components/alphaess/config_flow.py +++ b/custom_components/alphaess/config_flow.py @@ -5,7 +5,7 @@ from typing import Any import aiohttp -from .alphaesstesting import alphaess +from alphaess import alphaess import voluptuous as vol from homeassistant import config_entries @@ -25,7 +25,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" - client = alphaess(data["AppID"], data["AppSecret"], ipaddress=data["IPAddress"]) + client = alphaess.alphaess(data["AppID"], data["AppSecret"], ipaddress=data["IPAddress"]) try: await client.authenticate() diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 4189cb7..464ec53 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Union import aiohttp -from .alphaesstesting import alphaess +from alphaess import alphaess from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -289,7 +289,7 @@ async def parse_discharge_config(self, config: Dict) -> Dict[str, Any]: class AlphaESSDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching data from the API.""" - def __init__(self, hass: HomeAssistant, client: alphaess) -> None: + def __init__(self, hass: HomeAssistant, client: alphaess.alphaess) -> None: """Initialize coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) self.api = client diff --git a/custom_components/alphaess/manifest.json b/custom_components/alphaess/manifest.json index 0ec8438..0de4780 100644 --- a/custom_components/alphaess/manifest.json +++ b/custom_components/alphaess/manifest.json @@ -13,7 +13,7 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/CharlesGillanders/homeassistant-alphaESS/issues", "requirements": [ - "alphaessopenapi==0.0.13" + "alphaessopenapi==0.0.14" ], "version": "0.7.0" } From 53c7c7b85e67a8a02af5f5441deee28ffb6700a9 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Jun 2025 18:55:51 +0930 Subject: [PATCH 12/19] Misc fixes --- custom_components/alphaess/button.py | 4 +-- custom_components/alphaess/config_flow.py | 4 +-- custom_components/alphaess/coordinator.py | 36 +++++++++---------- custom_components/alphaess/manifest.json | 2 +- custom_components/alphaess/number.py | 2 +- custom_components/alphaess/sensor.py | 6 ++-- .../alphaess/translations/en.json | 4 +-- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/custom_components/alphaess/button.py b/custom_components/alphaess/button.py index 931d971..392ee00 100644 --- a/custom_components/alphaess/button.py +++ b/custom_components/alphaess/button.py @@ -94,7 +94,7 @@ def __init__(self, coordinator, config, serial, key_supported_states, ev_charger model_id=coordinator.data[invertor]["EV Charger S/N"], name=f"Alpha ESS Charger : {coordinator.data[invertor]["EV Charger S/N"]}", ) - elif "Local IP" in coordinator.data[invertor]: + elif "Local IP" in coordinator.data[invertor] and coordinator.data[invertor].get('Local IP') != '0': self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, serial)}, @@ -104,7 +104,7 @@ def __init__(self, coordinator, config, serial, key_supported_states, ev_charger manufacturer="AlphaESS", model=coordinator.data[invertor]["Model"], model_id=self._serial, - name=f"Alpha ESS Energy Statistics LOCAL : {serial}", + name=f"Alpha ESS Energy Statistics : {serial}", configuration_url=f"http://{coordinator.data[invertor]["Local IP"]}" ) elif self._serial == serial: diff --git a/custom_components/alphaess/config_flow.py b/custom_components/alphaess/config_flow.py index 532095a..8526f84 100644 --- a/custom_components/alphaess/config_flow.py +++ b/custom_components/alphaess/config_flow.py @@ -18,14 +18,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({ vol.Required("AppID", description="AppID"): str, vol.Required("AppSecret", description="AppSecret"): str, - vol.Optional("IPAddress", default=None): vol.Any(None, str) + vol.Optional("IPAddress", default='0'): vol.Any(None, str) }) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" - client = alphaess.alphaess(data["AppID"], data["AppSecret"], ipaddress=data["IPAddress"]) + client = alphaess(data["AppID"], data["AppSecret"], ipaddress=data["IPAddress"]) try: await client.authenticate() diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 464ec53..2d5c0b2 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -95,23 +95,6 @@ async def parse_basic_info(self, invertor: Dict) -> Dict[str, Any]: "Installed Capacity": await self.dp.process_value(invertor.get("cobat")), } - async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[str, Any]: - """Parse EV charger data.""" - if not ev_data: - return {} - - ev_data = ev_data[0] if isinstance(ev_data, list) else ev_data - ev_status = invertor.get("EVStatus", {}) - ev_current = invertor.get("EVCurrent", {}) - - return { - "EV Charger S/N": await self.dp.safe_get(ev_data, "evchargerSn"), - "EV Charger Model": await self.dp.safe_get(ev_data, "evchargerModel"), - "EV Charger Status": await self.dp.safe_get(ev_status, "evchargerStatus"), - "EV Charger Status Raw": await self.dp.safe_get(ev_status, "evchargerStatus"), - "Household current setup": await self.dp.safe_get(ev_current, "currentsetting"), - } - async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: """Parse local IP system data.""" if not local_ip_data: @@ -141,6 +124,23 @@ async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: "4G Module": await self.dp.safe_get(device_info, "g4moudle"), } + async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[str, Any]: + """Parse EV charger data.""" + if not ev_data: + return {} + + ev_data = ev_data[0] if isinstance(ev_data, list) else ev_data + ev_status = invertor.get("EVStatus", {}) + ev_current = invertor.get("EVCurrent", {}) + + return { + "EV Charger S/N": await self.dp.safe_get(ev_data, "evchargerSn"), + "EV Charger Model": await self.dp.safe_get(ev_data, "evchargerModel"), + "EV Charger Status": await self.dp.safe_get(ev_status, "evchargerStatus"), + "EV Charger Status Raw": await self.dp.safe_get(ev_status, "evchargerStatus"), + "Household current setup": await self.dp.safe_get(ev_current, "currentsetting"), + } + async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: """Parse summary statistics.""" data = { @@ -447,4 +447,4 @@ async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: bat_use_cap = discharge_config.get("batUseCap", 10) if discharge_config else 10 data["Charging Range"] = f"{bat_use_cap}% - {bat_high_cap}%" - return data \ No newline at end of file + return data diff --git a/custom_components/alphaess/manifest.json b/custom_components/alphaess/manifest.json index 0de4780..27148af 100644 --- a/custom_components/alphaess/manifest.json +++ b/custom_components/alphaess/manifest.json @@ -13,7 +13,7 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/CharlesGillanders/homeassistant-alphaESS/issues", "requirements": [ - "alphaessopenapi==0.0.14" + "alphaessopenapi==0.0.15" ], "version": "0.7.0" } diff --git a/custom_components/alphaess/number.py b/custom_components/alphaess/number.py index b885ef9..38d97d0 100644 --- a/custom_components/alphaess/number.py +++ b/custom_components/alphaess/number.py @@ -53,7 +53,7 @@ def __init__(self, coordinator, serial, config, full_number_supported_states, ha for invertor in coordinator.data: serial = invertor.upper() - if "Local IP" in coordinator.data[invertor]: + if "Local IP" in coordinator.data[invertor] and coordinator.data[invertor].get('Local IP') != '0': _LOGGER.info(f"INVERTER LOCAL DATA = {coordinator.data[invertor]}") self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 5bc0e76..58b489b 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -51,6 +51,8 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: _LOGGER.info(f"New Inverter: Serial: {serial}, Model: {model}") + _LOGGER.info("DATA RECEIVED IS: %s", data) + has_local_ip_data = 'Local IP' in data # This is done due to the limited data that inverters like the Storion-S5 support @@ -81,7 +83,7 @@ async def async_setup_entry(hass, entry, async_add_entities) -> None: ) ) - if has_local_ip_data: + if has_local_ip_data and data.get('Local IP') != '0' and data.get('Device Status') is not None: _LOGGER.info(f"New local IP system sensor for {serial}") for description in LOCAL_IP_SYSTEM_SENSORS: entities.append( @@ -142,7 +144,7 @@ def __init__(self, coordinator, config, serial, key_supported_states, currency, manufacturer="AlphaESS", model=coordinator.data[invertor]["Model"], model_id=self._serial, - name=f"Alpha ESS Energy Statistics LOCAL : {serial}", + name=f"Alpha ESS Energy Statistics : {serial}", configuration_url=f"http://{coordinator.data[invertor]["Local IP"]}" ) elif self._serial == serial: diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index 2fbbc65..d41b647 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -7,7 +7,7 @@ "data": { "AppID": "AppID", "AppSecret": "AppSecret", - "IPAddress": "IP Address (optional, for local API access)" + "IPAddress": "IP Address (for local API access), 0 to disable" } } }, @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "IPAddress": "IP Address (local API access) 0 to disable" + "IPAddress": "IP Address (local API access), 0 to disable" } } } From 1fd285241a511108a5bcbe62e56db1db13fc1a1c Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Mon, 30 Jun 2025 19:04:42 +0930 Subject: [PATCH 13/19] Change all strings within coordinator to AlphaESSNames use the --- custom_components/alphaess/coordinator.py | 161 +++++++++++----------- 1 file changed, 84 insertions(+), 77 deletions(-) diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 2d5c0b2..5202659 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -18,6 +18,7 @@ get_inverter_list, LOWER_INVERTER_API_CALL_LIST ) +from .enums import AlphaESSNames _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -86,13 +87,13 @@ async def parse_basic_info(self, invertor: Dict) -> Dict[str, Any]: """Parse basic inverter information.""" return { "Model": await self.dp.process_value(invertor.get("minv")), - "Battery Model": await self.dp.process_value(invertor.get("mbat")), - "Inverter nominal Power": await self.dp.process_value(invertor.get("poinv")), - "Pv nominal Power": await self.dp.process_value(invertor.get("popv")), - "EMS Status": await self.dp.process_value(invertor.get("emsStatus")), - "Maximum Battery Capacity": await self.dp.process_value(invertor.get("usCapacity")), - "Current Capacity": await self.dp.process_value(invertor.get("surplusCobat")), - "Installed Capacity": await self.dp.process_value(invertor.get("cobat")), + AlphaESSNames.mbat: await self.dp.process_value(invertor.get("mbat")), + AlphaESSNames.poinv: await self.dp.process_value(invertor.get("poinv")), + AlphaESSNames.popv: await self.dp.process_value(invertor.get("popv")), + AlphaESSNames.EmsStatus: await self.dp.process_value(invertor.get("emsStatus")), + AlphaESSNames.usCapacity: await self.dp.process_value(invertor.get("usCapacity")), + AlphaESSNames.surplusCobat: await self.dp.process_value(invertor.get("surplusCobat")), + AlphaESSNames.cobat: await self.dp.process_value(invertor.get("cobat")), } async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: @@ -104,24 +105,24 @@ async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: device_info = local_ip_data.get("device_info", {}) return { - "Local IP": local_ip_data.get("ip"), - "Device Status": await self.dp.safe_get(status, "devstatus"), - "Cloud Connection Status": await self.dp.safe_get(status, "serverstatus"), - "WiFi Status": await self.dp.safe_get(status, "wifistatus"), - "Connected SSID": await self.dp.safe_get(status, "connssid"), - "WiFi DHCP": await self.dp.safe_get(status, "wifidhcp"), - "WiFi IP": await self.dp.safe_get(status, "wifiip"), - "WiFi Mask": await self.dp.safe_get(status, "wifimask"), - "WiFi Gateway": await self.dp.safe_get(status, "wifigateway"), - "Device Serial Number": await self.dp.safe_get(device_info, "sn"), - "Device Key": await self.dp.safe_get(device_info, "key"), - "Hardware Version": await self.dp.safe_get(device_info, "hw"), - "Software Version": await self.dp.safe_get(device_info, "sw"), - "APN": await self.dp.safe_get(device_info, "apn"), - "Username": await self.dp.safe_get(device_info, "username"), - "Password": await self.dp.safe_get(device_info, "password"), - "Ethernet Module": await self.dp.safe_get(device_info, "ethmoudle"), - "4G Module": await self.dp.safe_get(device_info, "g4moudle"), + AlphaESSNames.localIP: local_ip_data.get("ip"), + AlphaESSNames.deviceStatus: await self.dp.safe_get(status, "devstatus"), + AlphaESSNames.cloudConnectionStatus: await self.dp.safe_get(status, "serverstatus"), + AlphaESSNames.wifiStatus: await self.dp.safe_get(status, "wifistatus"), + AlphaESSNames.connectedSSID: await self.dp.safe_get(status, "connssid"), + AlphaESSNames.wifiDHCP: await self.dp.safe_get(status, "wifidhcp"), + AlphaESSNames.wifiIP: await self.dp.safe_get(status, "wifiip"), + AlphaESSNames.wifiMask: await self.dp.safe_get(status, "wifimask"), + AlphaESSNames.wifiGateway: await self.dp.safe_get(status, "wifigateway"), + AlphaESSNames.deviceSerialNumber: await self.dp.safe_get(device_info, "sn"), + AlphaESSNames.deviceKey: await self.dp.safe_get(device_info, "key"), + AlphaESSNames.hardwareVersion: await self.dp.safe_get(device_info, "hw"), + AlphaESSNames.softwareVersion: await self.dp.safe_get(device_info, "sw"), + AlphaESSNames.apn: await self.dp.safe_get(device_info, "apn"), + AlphaESSNames.username: await self.dp.safe_get(device_info, "username"), + AlphaESSNames.password: await self.dp.safe_get(device_info, "password"), + AlphaESSNames.ethernetModule: await self.dp.safe_get(device_info, "ethmoudle"), + AlphaESSNames.fourGModule: await self.dp.safe_get(device_info, "g4moudle"), } async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[str, Any]: @@ -134,21 +135,21 @@ async def parse_ev_data(self, ev_data: Optional[Dict], invertor: Dict) -> Dict[s ev_current = invertor.get("EVCurrent", {}) return { - "EV Charger S/N": await self.dp.safe_get(ev_data, "evchargerSn"), - "EV Charger Model": await self.dp.safe_get(ev_data, "evchargerModel"), - "EV Charger Status": await self.dp.safe_get(ev_status, "evchargerStatus"), - "EV Charger Status Raw": await self.dp.safe_get(ev_status, "evchargerStatus"), - "Household current setup": await self.dp.safe_get(ev_current, "currentsetting"), + AlphaESSNames.evchargersn: await self.dp.safe_get(ev_data, "evchargerSn"), + AlphaESSNames.evchargermodel: await self.dp.safe_get(ev_data, "evchargerModel"), + AlphaESSNames.evchargerstatus: await self.dp.safe_get(ev_status, "evchargerStatus"), + AlphaESSNames.evchargerstatusraw: await self.dp.safe_get(ev_status, "evchargerStatus"), + AlphaESSNames.evcurrentsetting: await self.dp.safe_get(ev_current, "currentsetting"), } async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: """Parse summary statistics.""" data = { - "Total Load": await self.dp.safe_get(sum_data, "eload"), - "Total Income": await self.dp.safe_get(sum_data, "totalIncome"), - "Total Generation": await self.dp.safe_get(sum_data, "epvtotal"), - "Trees Planted": await self.dp.safe_get(sum_data, "treeNum"), - "Co2 Reduction": await self.dp.safe_get(sum_data, "carbonNum"), + AlphaESSNames.TotalLoad: await self.dp.safe_get(sum_data, "eload"), + AlphaESSNames.Income: await self.dp.safe_get(sum_data, "totalIncome"), + AlphaESSNames.Total_Generation: await self.dp.safe_get(sum_data, "epvtotal"), + AlphaESSNames.treePlanted: await self.dp.safe_get(sum_data, "treeNum"), + AlphaESSNames.carbonReduction: await self.dp.safe_get(sum_data, "carbonNum"), "Currency": await self.dp.safe_get(sum_data, "moneyType"), } @@ -156,8 +157,8 @@ async def parse_summary_data(self, sum_data: Dict) -> Dict[str, Any]: self_consumption = await self.dp.safe_get(sum_data, "eselfConsumption") self_sufficiency = await self.dp.safe_get(sum_data, "eselfSufficiency") - data["Self Consumption"] = self_consumption * 100 if self_consumption is not None else None - data["Self Sufficiency"] = self_sufficiency * 100 if self_sufficiency is not None else None + data[AlphaESSNames.SelfConsumption] = self_consumption * 100 if self_consumption is not None else None + data[AlphaESSNames.SelfSufficiency] = self_sufficiency * 100 if self_sufficiency is not None else None return data @@ -169,15 +170,15 @@ async def parse_energy_data(self, energy_data: Dict) -> Dict[str, Any]: charge = await self.dp.safe_get(energy_data, "eCharge") return { - "Solar Production": pv, - "Solar to Load": await self.dp.safe_calculate(pv, feedin), - "Solar to Grid": feedin, - "Solar to Battery": await self.dp.safe_calculate(charge, gridcharge), - "Grid to Load": await self.dp.safe_get(energy_data, "eInput"), - "Grid to Battery": gridcharge, - "Charge": charge, - "Discharge": await self.dp.safe_get(energy_data, "eDischarge"), - "EV Charger": await self.dp.safe_get(energy_data, "eChargingPile"), + 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.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"), } async def parse_power_data(self, power_data: Dict, one_day_power: Optional[list]) -> Dict[str, Any]: @@ -188,46 +189,49 @@ async def parse_power_data(self, power_data: Dict, one_day_power: Optional[list] ev_details = power_data.get("pevDetail", {}) data = { - "Instantaneous Battery SOC": soc, - "Instantaneous Battery I/O": await self.dp.safe_get(power_data, "pbat"), - "Instantaneous Load": await self.dp.safe_get(power_data, "pload"), - "Instantaneous Generation": await self.dp.safe_get(power_data, "ppv"), - "Instantaneous Grid I/O Total": await self.dp.safe_get(power_data, "pgrid"), - "pev": await self.dp.safe_get(power_data, "pev"), - "PrealL1": await self.dp.safe_get(power_data, "prealL1"), - "PrealL2": await self.dp.safe_get(power_data, "prealL2"), - "PrealL3": await self.dp.safe_get(power_data, "prealL3"), + AlphaESSNames.BatterySOC: soc, + AlphaESSNames.BatteryIO: await self.dp.safe_get(power_data, "pbat"), + AlphaESSNames.Load: await self.dp.safe_get(power_data, "pload"), + AlphaESSNames.Generation: await self.dp.safe_get(power_data, "ppv"), + AlphaESSNames.GridIOTotal: await self.dp.safe_get(power_data, "pgrid"), + AlphaESSNames.pev: await self.dp.safe_get(power_data, "pev"), + AlphaESSNames.PrealL1: await self.dp.safe_get(power_data, "prealL1"), + AlphaESSNames.PrealL2: await self.dp.safe_get(power_data, "prealL2"), + AlphaESSNames.PrealL3: await self.dp.safe_get(power_data, "prealL3"), } # PV string data for i in range(1, 5): - data[f"Instantaneous PPV{i}"] = await self.dp.safe_get(pv_details, f"ppv{i}") + data[getattr(AlphaESSNames, f"PPV{i}")] = await self.dp.safe_get(pv_details, f"ppv{i}") - data["pmeterDc"] = await self.dp.safe_get(pv_details, "pmeterDc") + data[AlphaESSNames.pmeterDc] = await self.dp.safe_get(pv_details, "pmeterDc") # Grid phase data for i in range(1, 4): - data[f"Instantaneous Grid I/O L{i}"] = await self.dp.safe_get(grid_details, f"pmeterL{i}") + data[getattr(AlphaESSNames, f"GridIOL{i}")] = await self.dp.safe_get(grid_details, f"pmeterL{i}") # EV power data for i in range(1, 5): - key = ["One", "Two", "Three", "Four"][i - 1] - data[f"Electric Vehicle Power {key}"] = await self.dp.safe_get(ev_details, f"ev{i}Power") + 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") # Fallback SOC from daily data if one_day_power and soc == 0: first_entry = one_day_power[0] cbat = first_entry.get("cbat") if cbat is not None: - data["State of Charge"] = cbat + data[AlphaESSNames.StateOfCharge] = cbat return data async def parse_charge_config(self, config: Dict) -> Dict[str, Any]: """Parse charge configuration.""" data = {} - for key in ["gridCharge", "batHighCap"]: - data[key] = await self.dp.safe_get(config, key) + for key in ["gridCharge", AlphaESSNames.batHighCap]: + if key == AlphaESSNames.batHighCap: + data[key] = await self.dp.safe_get(config, "batHighCap") + else: + data[key] = await self.dp.safe_get(config, key) # Parse time slots with the correct key names time_start_1 = await self.dp.safe_get(config, "timeChaf1") @@ -237,14 +241,14 @@ async def parse_charge_config(self, config: Dict) -> Dict[str, Any]: # Format as "HH:MM - HH:MM" to match expected format if time_start_1 and time_end_1: - data["Charge Time 1"] = f"{time_start_1} - {time_end_1}" + data[AlphaESSNames.ChargeTime1] = f"{time_start_1} - {time_end_1}" else: - data["Charge Time 1"] = "00:00 - 00:00" + data[AlphaESSNames.ChargeTime1] = "00:00 - 00:00" if time_start_2 and time_end_2: - data["Charge Time 2"] = f"{time_start_2} - {time_end_2}" + data[AlphaESSNames.ChargeTime2] = f"{time_start_2} - {time_end_2}" else: - data["Charge Time 2"] = "00:00 - 00:00" + data[AlphaESSNames.ChargeTime2] = "00:00 - 00:00" # Also keep the raw values for compatibility data["charge_timeChaf1"] = time_start_1 @@ -257,8 +261,11 @@ async def parse_charge_config(self, config: Dict) -> Dict[str, Any]: async def parse_discharge_config(self, config: Dict) -> Dict[str, Any]: """Parse discharge configuration.""" data = {} - for key in ["ctrDis", "batUseCap"]: - data[key] = await self.dp.safe_get(config, key) + for key in ["ctrDis", AlphaESSNames.batUseCap]: + if key == AlphaESSNames.batUseCap: + data[key] = await self.dp.safe_get(config, "batUseCap") + else: + data[key] = await self.dp.safe_get(config, key) # Parse time slots with the correct key names time_start_1 = await self.dp.safe_get(config, "timeDisf1") @@ -268,14 +275,14 @@ async def parse_discharge_config(self, config: Dict) -> Dict[str, Any]: # Format as "HH:MM - HH:MM" to match expected format if time_start_1 and time_end_1: - data["Discharge Time 1"] = f"{time_start_1} - {time_end_1}" + data[AlphaESSNames.DischargeTime1] = f"{time_start_1} - {time_end_1}" else: - data["Discharge Time 1"] = "00:00 - 00:00" + data[AlphaESSNames.DischargeTime1] = "00:00 - 00:00" if time_start_2 and time_end_2: - data["Discharge Time 2"] = f"{time_start_2} - {time_end_2}" + data[AlphaESSNames.DischargeTime2] = f"{time_start_2} - {time_end_2}" else: - data["Discharge Time 2"] = "00:00 - 00:00" + data[AlphaESSNames.DischargeTime2] = "00:00 - 00:00" # Also keep the raw values for compatibility data["discharge_timeDisf1"] = time_start_1 @@ -327,8 +334,8 @@ async def control_ev(self, serial: str, ev_serial: str, direction: str) -> None: async def reset_config(self, serial: str) -> None: """Reset charge and discharge configuration.""" - bat_use_cap = self.hass.data[DOMAIN][serial].get("batUseCap", 10) - bat_high_cap = self.hass.data[DOMAIN][serial].get("batHighCap", 90) + bat_use_cap = self.hass.data[DOMAIN][serial].get(AlphaESSNames.batUseCap, 10) + bat_high_cap = self.hass.data[DOMAIN][serial].get(AlphaESSNames.batHighCap, 90) results = await self._reset_charge_discharge_config(serial, bat_high_cap, bat_use_cap) _LOGGER.info( @@ -445,6 +452,6 @@ async def _parse_inverter_data(self, invertor: Dict) -> Dict[str, Any]: if charge_config or discharge_config: bat_high_cap = charge_config.get("batHighCap", 90) if charge_config else 90 bat_use_cap = discharge_config.get("batUseCap", 10) if discharge_config else 10 - data["Charging Range"] = f"{bat_use_cap}% - {bat_high_cap}%" + data[AlphaESSNames.ChargeRange] = f"{bat_use_cap}% - {bat_high_cap}%" - return data + return data \ No newline at end of file From f89660ad3e26d151b5805f69b3735db81312a842 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Tue, 1 Jul 2025 22:01:52 +0930 Subject: [PATCH 14/19] Fix for charge/discharge times not showing --- custom_components/alphaess/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 58b489b..50bf7d5 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -182,6 +182,10 @@ def native_value(self) -> StateType: return EV_CHARGER_STATE_KEYS.get(raw_state, "unknown") + if self._key in [AlphaESSNames.ChargeTime1, AlphaESSNames.ChargeTime2, + AlphaESSNames.DischargeTime1, AlphaESSNames.DischargeTime2]: + return self._coordinator.data.get(self._serial, {}).get(self._key) + # Normal sensor handling return self._coordinator.data.get(self._serial, {}).get(self._name) From 7c5f02f00389bb10b43cf2e787ccebddf01ad803 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Tue, 1 Jul 2025 22:22:05 +0930 Subject: [PATCH 15/19] Add support for showing cloud status --- custom_components/alphaess/const.py | 17 +++++++++++ custom_components/alphaess/sensor.py | 30 +++++++++++++++---- custom_components/alphaess/sensorlist.py | 1 + .../alphaess/translations/en.json | 21 ++++++++++++- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index 506b261..bed61db 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -49,6 +49,23 @@ 9: "faulted" } +TCP_STATUS_KEYS = { + 0: "connected_ok", + -1: "initialization", + -2: "not_connected_router", + -3: "dns_lookup_error", + -4: "connect_fail", + -5: "signal_too_weak", + -6: "failed_register_base_station", + -7: "sim_card_not_inserted", + -8: "not_bound_plant", + -9: "key_error", + -10: "sn_error", + -11: "communication_timeout", + -12: "communication_abort_server", + -13: "server_address_error" +} + def increment_inverter_count(): global INVERTER_COUNT INVERTER_COUNT += 1 diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 50bf7d5..0a1f9b5 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -14,7 +14,7 @@ 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 +from .const import DOMAIN, LIMITED_INVERTER_SENSOR_LIST, EV_CHARGER_STATE_KEYS, TCP_STATUS_KEYS from .coordinator import AlphaESSDataUpdateCoordinator _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -175,19 +175,29 @@ def native_value(self) -> StateType: return None if self._key == AlphaESSNames.evchargerstatus: - raw_state = self._coordinator.data.get(self._serial, {}).get(self._name) - + 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") + # Add TCP status handling - you'll need to add the actual key name for TCP status + if self._key == AlphaESSNames.cloudConnectionStatus: # or whatever the TCP status key is + raw_state = self._coordinator.data.get(self._serial, {}).get(self._key) + if raw_state is None: + return None + try: + tcp_status = int(raw_state) + return TCP_STATUS_KEYS.get(tcp_status, "connect_fail") + except (ValueError, TypeError): + return "connect_fail" + + # For time-based sensors if self._key in [AlphaESSNames.ChargeTime1, AlphaESSNames.ChargeTime2, AlphaESSNames.DischargeTime1, AlphaESSNames.DischargeTime2]: return self._coordinator.data.get(self._serial, {}).get(self._key) # Normal sensor handling - return self._coordinator.data.get(self._serial, {}).get(self._name) + return self._coordinator.data.get(self._serial, {}).get(self._key) @property def native_unit_of_measurement(self): @@ -206,6 +216,12 @@ def options(self) -> list[str] | None: return ["available", "preparing", "charging", "suspended_evse", "suspended_ev", "finishing", "faulted", "unknown"] + if self._key == AlphaESSNames.cloudConnectionStatus: + return ["connected_ok", "initialization", "not_connected_router", "dns_lookup_error", + "connect_fail", "signal_too_weak", "failed_register_base_station", + "sim_card_not_inserted", "not_bound_plant", "key_error", "sn_error", + "communication_timeout", "communication_abort_server", "server_address_error"] + return None @property @@ -213,6 +229,8 @@ def translation_key(self) -> str | None: """Return the translation key.""" if self._key == AlphaESSNames.evchargerstatus: return "ev_charger_status" + if self._key == AlphaESSNames.cloudConnectionStatus: + return "tcp_status" return None @property @@ -256,4 +274,4 @@ def get_time_range(prefix): elif direction == "Charge": return get_time_range("charge") - return None + return None \ No newline at end of file diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index abf021a..f25bc56 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -811,6 +811,7 @@ key=AlphaESSNames.cloudConnectionStatus, name="Cloud Connection Status", icon="mdi:cloud-check", + device_class=SensorDeviceClass.ENUM, native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index d41b647..e6a5237 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -43,7 +43,26 @@ "faulted": "Faulted", "unknown": "Unknown" } + }, + "tcp_status": { + "name": "TCP Status", + "state": { + "connected_ok": "Connected OK", + "initialization": "Initialization", + "not_connected_router": "Not connected to a router", + "dns_lookup_error": "DNS lookup error", + "connect_fail": "Connect fail", + "signal_too_weak": "Signal too weak", + "failed_register_base_station": "Failed to register base station", + "sim_card_not_inserted": "SIM Card not inserted", + "not_bound_plant": "Not bound to a plant", + "key_error": "KEY error", + "sn_error": "SN error", + "communication_timeout": "Communication timeout", + "communication_abort_server": "Communication abort by server", + "server_address_error": "Server address error" + } } } } -} +} \ No newline at end of file From 741b96662a74ac7c56066c14ca9944caa926353b Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Tue, 1 Jul 2025 22:34:27 +0930 Subject: [PATCH 16/19] Add new Ethernet Status & 4G Status sensors --- custom_components/alphaess/const.py | 15 ++++++ custom_components/alphaess/sensor.py | 47 +++++++++++++++---- custom_components/alphaess/sensorlist.py | 17 ++++++- .../alphaess/translations/en.json | 17 +++++++ 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index bed61db..7b19149 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -66,6 +66,21 @@ -13: "server_address_error" } +ETHERNET_STATUS_KEYS = { + 0: "link_up", + # All other values default to link_down +} + +FOUR_G_STATUS_KEYS = { + 0: "ok", + -1: "initialization", + -2: "connected_fail", + -3: "connected_lost", + -4: "connected_fail" + # All other values default to unknown_error +} + + def increment_inverter_count(): global INVERTER_COUNT INVERTER_COUNT += 1 diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 0a1f9b5..645cfa5 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -3,7 +3,7 @@ from typing import List from homeassistant.components.sensor import ( - SensorEntity + SensorEntity, SensorDeviceClass ) from homeassistant.const import CURRENCY_DOLLAR from homeassistant.helpers.typing import StateType @@ -14,7 +14,7 @@ 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 +from .const import DOMAIN, LIMITED_INVERTER_SENSOR_LIST, EV_CHARGER_STATE_KEYS, TCP_STATUS_KEYS, ETHERNET_STATUS_KEYS, FOUR_G_STATUS_KEYS from .coordinator import AlphaESSDataUpdateCoordinator _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -174,14 +174,15 @@ def native_value(self) -> StateType: if self._coordinator.data is None: return None + # Handle EV charger status enum if self._key == AlphaESSNames.evchargerstatus: 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") - # Add TCP status handling - you'll need to add the actual key name for TCP status - if self._key == AlphaESSNames.cloudConnectionStatus: # or whatever the TCP status key is + # Handle TCP status for cloud connection + if self._key == AlphaESSNames.cloudConnectionStatus: raw_state = self._coordinator.data.get(self._serial, {}).get(self._key) if raw_state is None: return None @@ -191,12 +192,32 @@ def native_value(self) -> StateType: except (ValueError, TypeError): return "connect_fail" - # For time-based sensors + # Handle Ethernet status + if self._key == AlphaESSNames.ethernetModule: + raw_state = self._coordinator.data.get(self._serial, {}).get(self._key) + if raw_state is None: + return None + try: + eth_status = int(raw_state) + return ETHERNET_STATUS_KEYS.get(eth_status, "link_down") + except (ValueError, TypeError): + return "link_down" + + # Handle 4G status + if self._key == AlphaESSNames.fourGModule: + raw_state = self._coordinator.data.get(self._serial, {}).get(self._key) + if raw_state is None: + return None + try: + g4_status = int(raw_state) + return FOUR_G_STATUS_KEYS.get(g4_status, "unknown_error") + except (ValueError, TypeError): + return "unknown_error" + if self._key in [AlphaESSNames.ChargeTime1, AlphaESSNames.ChargeTime2, AlphaESSNames.DischargeTime1, AlphaESSNames.DischargeTime2]: return self._coordinator.data.get(self._serial, {}).get(self._key) - # Normal sensor handling return self._coordinator.data.get(self._serial, {}).get(self._key) @property @@ -222,15 +243,25 @@ def options(self) -> list[str] | None: "sim_card_not_inserted", "not_bound_plant", "key_error", "sn_error", "communication_timeout", "communication_abort_server", "server_address_error"] + if self._key == AlphaESSNames.ethernetModule: + return ["link_up", "link_down"] + + if self._key == AlphaESSNames.fourGModule: + return ["ok", "initialization", "connected_fail", "connected_lost", "unknown_error"] + return None @property def translation_key(self) -> str | None: """Return the translation key.""" - if self._key == AlphaESSNames.evchargerstatus: + if self._key == AlphaESSNames.evchargerstatus and self._device_class == SensorDeviceClass.ENUM: return "ev_charger_status" - if self._key == AlphaESSNames.cloudConnectionStatus: + if self._key == AlphaESSNames.cloudConnectionStatus and self._device_class == SensorDeviceClass.ENUM: return "tcp_status" + if self._key == AlphaESSNames.ethernetModule and self._device_class == SensorDeviceClass.ENUM: + return "ethernet_status" + if self._key == AlphaESSNames.fourGModule and self._device_class == SensorDeviceClass.ENUM: + return "four_g_status" return None @property diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index f25bc56..e7424e8 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -816,6 +816,22 @@ state_class=None, entity_category=EntityCategory.DIAGNOSTIC, ), + AlphaESSSensorDescription( + key=AlphaESSNames.ethernetModule, + name="Ethernet Status", + device_class=SensorDeviceClass.ENUM, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), + AlphaESSSensorDescription( + key=AlphaESSNames.fourGModule, + name="4G Status", + device_class=SensorDeviceClass.ENUM, + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), ] EV_CHARGING_DETAILS: List[AlphaESSSensorDescription] = [ @@ -858,4 +874,3 @@ ) ] - diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index e6a5237..d2be9e4 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -62,6 +62,23 @@ "communication_abort_server": "Communication abort by server", "server_address_error": "Server address error" } + }, + "ethernet_status": { + "name": "Ethernet Status", + "state": { + "link_up": "Link Up", + "link_down": "Link Down" + } + }, + "four_g_status": { + "name": "4G Status", + "state": { + "ok": "OK", + "initialization": "Initialization", + "connected_fail": "Connected fail", + "connected_lost": "Connected lost", + "unknown_error": "Unknown Error" + } } } } From 59d8c6ab9ff6c81ecb026d77a32d86549267ed94 Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Tue, 1 Jul 2025 22:45:07 +0930 Subject: [PATCH 17/19] Add wifi status and register Key --- custom_components/alphaess/const.py | 10 +++++++++ custom_components/alphaess/coordinator.py | 2 +- custom_components/alphaess/enums.py | 2 +- custom_components/alphaess/sensor.py | 21 ++++++++++++++++++- custom_components/alphaess/sensorlist.py | 8 +++++++ .../alphaess/translations/en.json | 12 +++++++++++ 6 files changed, 52 insertions(+), 3 deletions(-) diff --git a/custom_components/alphaess/const.py b/custom_components/alphaess/const.py index 7b19149..a97ea17 100644 --- a/custom_components/alphaess/const.py +++ b/custom_components/alphaess/const.py @@ -66,6 +66,16 @@ -13: "server_address_error" } +WIFI_STATUS_KEYS = { + 0: "connection_idle", + 1: "connecting", + 2: "password_error", + 3: "ap_not_found", + 4: "connect_fail", + 5: "connected_ok" + # All other values default to unknown_error +} + ETHERNET_STATUS_KEYS = { 0: "link_up", # All other values default to link_down diff --git a/custom_components/alphaess/coordinator.py b/custom_components/alphaess/coordinator.py index 5202659..fa9a06d 100644 --- a/custom_components/alphaess/coordinator.py +++ b/custom_components/alphaess/coordinator.py @@ -115,7 +115,7 @@ async def parse_local_ip_data(self, local_ip_data: Dict) -> Dict[str, Any]: AlphaESSNames.wifiMask: await self.dp.safe_get(status, "wifimask"), AlphaESSNames.wifiGateway: await self.dp.safe_get(status, "wifigateway"), AlphaESSNames.deviceSerialNumber: await self.dp.safe_get(device_info, "sn"), - AlphaESSNames.deviceKey: await self.dp.safe_get(device_info, "key"), + AlphaESSNames.registerKey: await self.dp.safe_get(device_info, "key"), AlphaESSNames.hardwareVersion: await self.dp.safe_get(device_info, "hw"), AlphaESSNames.softwareVersion: await self.dp.safe_get(device_info, "sw"), AlphaESSNames.apn: await self.dp.safe_get(device_info, "apn"), diff --git a/custom_components/alphaess/enums.py b/custom_components/alphaess/enums.py index 8c15e0b..152dcdf 100644 --- a/custom_components/alphaess/enums.py +++ b/custom_components/alphaess/enums.py @@ -86,7 +86,7 @@ class AlphaESSNames(str, Enum): wifiMask = "WiFi Mask" wifiGateway = "WiFi Gateway" deviceSerialNumber = "Device Serial Number" - deviceKey = "Device Key" + registerKey = "Register Key" apn = "APN" username = "Username" password = "Password" diff --git a/custom_components/alphaess/sensor.py b/custom_components/alphaess/sensor.py index 645cfa5..764ce34 100644 --- a/custom_components/alphaess/sensor.py +++ b/custom_components/alphaess/sensor.py @@ -14,7 +14,8 @@ 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 +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 from .coordinator import AlphaESSDataUpdateCoordinator _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -214,10 +215,22 @@ def native_value(self) -> StateType: except (ValueError, TypeError): return "unknown_error" + # Handle WiFi status + if self._key == AlphaESSNames.wifiStatus: + raw_state = self._coordinator.data.get(self._serial, {}).get(self._key) + if raw_state is None: + return None + try: + wifi_status = int(raw_state) + return WIFI_STATUS_KEYS.get(wifi_status, "unknown_error") + except (ValueError, TypeError): + return "unknown_error" + if self._key in [AlphaESSNames.ChargeTime1, AlphaESSNames.ChargeTime2, AlphaESSNames.DischargeTime1, AlphaESSNames.DischargeTime2]: return self._coordinator.data.get(self._serial, {}).get(self._key) + # Normal sensor handling - use the key instead of name for consistency return self._coordinator.data.get(self._serial, {}).get(self._key) @property @@ -249,6 +262,10 @@ def options(self) -> list[str] | None: if self._key == AlphaESSNames.fourGModule: return ["ok", "initialization", "connected_fail", "connected_lost", "unknown_error"] + if self._key == AlphaESSNames.wifiStatus: + return ["connection_idle", "connecting", "password_error", "ap_not_found", + "connect_fail", "connected_ok", "unknown_error"] + return None @property @@ -262,6 +279,8 @@ def translation_key(self) -> str | None: return "ethernet_status" if self._key == AlphaESSNames.fourGModule and self._device_class == SensorDeviceClass.ENUM: return "four_g_status" + if self._key == AlphaESSNames.wifiStatus and self._device_class == SensorDeviceClass.ENUM: + return "wifi_status" return None @property diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index e7424e8..7db6935 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -771,6 +771,7 @@ key=AlphaESSNames.wifiStatus, name="WiFi Status", icon="mdi:wifi", + device_class=SensorDeviceClass.ENUM, native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, @@ -832,6 +833,13 @@ state_class=None, entity_category=EntityCategory.DIAGNOSTIC, ), + AlphaESSSensorDescription( + key=AlphaESSNames.registerKey, + name="Register Key", + native_unit_of_measurement=None, + state_class=None, + entity_category=EntityCategory.DIAGNOSTIC, + ), ] EV_CHARGING_DETAILS: List[AlphaESSSensorDescription] = [ diff --git a/custom_components/alphaess/translations/en.json b/custom_components/alphaess/translations/en.json index d2be9e4..5f68aff 100644 --- a/custom_components/alphaess/translations/en.json +++ b/custom_components/alphaess/translations/en.json @@ -79,6 +79,18 @@ "connected_lost": "Connected lost", "unknown_error": "Unknown Error" } + }, + "wifi_status": { + "name": "WiFi Status", + "state": { + "connection_idle": "Connection Idle", + "connecting": "Connecting...", + "password_error": "Password Error", + "ap_not_found": "AP Not Found", + "connect_fail": "Connect Fail", + "connected_ok": "Connected OK", + "unknown_error": "Unknown Error" + } } } } From 7e83295cb9d8ba1441571822824ca98ed076813b Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Tue, 1 Jul 2025 23:12:09 +0930 Subject: [PATCH 18/19] Add documentation --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 9b4649f..49e4172 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,19 @@ The current charge config, discharge config and charging range will only update 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 + +To use the local inverter support, you will need to have a local inverter that is able to reach your HA instance (preferably on the same subnet). + +To add a local inverter to an existing AlphaESS integration, you will need to select the "Configure" option from the AlphaESS integration in Home Assistant, and then input your inverter's IP address, you can also do this if you need to reconfigure your inverter's IP address (due to DHCP changes, etc). + +To remove/reset the local inverter integration, you will need to go back to the configuration settings, and set it to 0. (this will "remove" all the sensors linked, and will need to be manually deleted) + +For now, if you have more than one inverter linked to your OpenAPI Account, the local inverter settings will only work on the first inverter that is linked to your account. support for setting it to be a custom one is coming. + +![](https://i.imgur.com/rHWI2gh.png) + + ## Issues with registering systems to the AlphaESS OpenAPI There has been a few issues regarding registering systems to the AlphaESS OpenAPI. The following are some of the issues that have been reported and how to resolve them. From 0379e34835146bd965cc7c6e727756b64efa2aea Mon Sep 17 00:00:00 2001 From: Joshua Leaper Date: Tue, 1 Jul 2025 23:15:33 +0930 Subject: [PATCH 19/19] Update sensors icons for new local inverters --- custom_components/alphaess/sensorlist.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/alphaess/sensorlist.py b/custom_components/alphaess/sensorlist.py index 7db6935..72195ce 100644 --- a/custom_components/alphaess/sensorlist.py +++ b/custom_components/alphaess/sensorlist.py @@ -779,7 +779,7 @@ AlphaESSSensorDescription( key=AlphaESSNames.connectedSSID, name="Connected SSID", - icon="mdi:wifi", + icon="mdi:wifi-marker", native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, @@ -811,7 +811,7 @@ AlphaESSSensorDescription( key=AlphaESSNames.cloudConnectionStatus, name="Cloud Connection Status", - icon="mdi:cloud-check", + icon="mdi:cloud-sync", device_class=SensorDeviceClass.ENUM, native_unit_of_measurement=None, state_class=None, @@ -824,6 +824,7 @@ native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:ethernet" ), AlphaESSSensorDescription( key=AlphaESSNames.fourGModule, @@ -832,6 +833,7 @@ native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:signal-4g" ), AlphaESSSensorDescription( key=AlphaESSNames.registerKey, @@ -839,6 +841,7 @@ native_unit_of_measurement=None, state_class=None, entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:key" ), ]