diff --git a/custom_components/rct_power/lib/const.py b/custom_components/rct_power/lib/const.py index 923e1da..ca32fb6 100644 --- a/custom_components/rct_power/lib/const.py +++ b/custom_components/rct_power/lib/const.py @@ -1,6 +1,6 @@ """Constants for RCT Power.""" # Base component constants -from enum import Enum, IntFlag, auto +from enum import Enum, IntFlag, auto, KEEP NAME = "RCT Power" @@ -53,7 +53,10 @@ class EntityUpdatePriority(Enum): STATIC = auto() -class BatteryStatusFlag(IntFlag): +class BatteryStatusFlag(IntFlag, boundary=KEEP): normal = 0 - calibrating = 1032 - balancing = 2048 + charging = 2**3 + discharging = 2**10 + balancing = 2**11 + + calibrating = charging | discharging diff --git a/custom_components/rct_power/lib/entities.py b/custom_components/rct_power/lib/entities.py index 8531218..de5222f 100644 --- a/custom_components/rct_power/lib/entities.py +++ b/custom_components/rct_power/lib/entities.py @@ -1,20 +1,19 @@ import re from typing import List -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import SensorStateClass from rctclient.registry import REGISTRY -from .const import BatteryStatusFlag -from .device_info_helpers import get_battery_device_info, get_inverter_device_info -from .entity import ( - EntityUpdatePriority, - RctPowerSensorEntityDescription, -) -from .state_helpers import ( - get_first_api_reponse_value_as_absolute_state, - get_first_api_response_value_as_battery_status, - sum_api_response_values_as_state, -) +from .device_info_helpers import get_battery_device_info +from .device_info_helpers import get_inverter_device_info +from .entity import EntityUpdatePriority +from .entity import RctPowerBitfieldSensorEntityDescription +from .entity import RctPowerSensorEntityDescription +from .state_helpers import available_battery_status +from .state_helpers import get_first_api_reponse_value_as_absolute_state +from .state_helpers import get_first_api_response_value_as_battery_status +from .state_helpers import sum_api_response_values_as_state def get_matching_names(expression: str): @@ -194,14 +193,6 @@ def get_matching_names(expression: str): update_priority=EntityUpdatePriority.INFREQUENT, state_class=SensorStateClass.TOTAL_INCREASING, ), - RctPowerSensorEntityDescription( - get_device_info=get_battery_device_info, - key="battery.bat_status", - name="Battery Status", - update_priority=EntityUpdatePriority.FREQUENT, - get_native_value=get_first_api_response_value_as_battery_status, - options=BatteryStatusFlag._member_names_, - ), ] inverter_sensor_entity_descriptions: List[RctPowerSensorEntityDescription] = [ @@ -711,8 +702,8 @@ def get_matching_names(expression: str): ), ] -fault_sensor_entity_descriptions: List[RctPowerSensorEntityDescription] = [ - RctPowerSensorEntityDescription( +bitfield_sensor_entity_descriptions: List[RctPowerBitfieldSensorEntityDescription] = [ + RctPowerBitfieldSensorEntityDescription( get_device_info=get_inverter_device_info, key="fault.flt", object_names=[ @@ -722,14 +713,23 @@ def get_matching_names(expression: str): "fault[3].flt", ], name="Faults", + update_priority=EntityUpdatePriority.FREQUENT, unique_id=f"{0x37F9D5CA}", # for backwards-compatibility ), + RctPowerBitfieldSensorEntityDescription( + get_device_info=get_battery_device_info, + key="battery.bat_status", + name="Battery Status", + update_priority=EntityUpdatePriority.FREQUENT, + get_native_value=get_first_api_response_value_as_battery_status, + options=available_battery_status, + ), ] sensor_entity_descriptions = [ *battery_sensor_entity_descriptions, *inverter_sensor_entity_descriptions, - *fault_sensor_entity_descriptions, + *bitfield_sensor_entity_descriptions, ] all_entity_descriptions = [ diff --git a/custom_components/rct_power/lib/entity.py b/custom_components/rct_power/lib/entity.py index d07207d..43be46a 100644 --- a/custom_components/rct_power/lib/entity.py +++ b/custom_components/rct_power/lib/entity.py @@ -31,9 +31,8 @@ from .device_class_helpers import guess_device_class_from_unit from .entry import RctPowerConfigEntryData from .multi_coordinator_entity import MultiCoordinatorEntity -from .state_helpers import ( - get_first_api_response_value_as_state, -) +from .state_helpers import get_api_response_values_as_bitfield +from .state_helpers import get_first_api_response_value_as_state from .update_coordinator import RctPowerDataUpdateCoordinator @@ -140,15 +139,18 @@ def device_info(self): class RctPowerSensorEntity(SensorEntity, RctPowerEntity): entity_description: "RctPowerSensorEntityDescription" # pyright: ignore [reportIncompatibleVariableOverride] + def get_valid_api_responses(self): + return [ + self.get_valid_api_response_value_by_id(object_info.object_id, None) + for object_info in self.object_infos + ] + @property def device_class(self): """Return the device class of the sensor.""" if device_class := super().device_class: return device_class - if self.options: - return SensorDeviceClass.ENUM - if self.native_unit_of_measurement: return guess_device_class_from_unit(self.native_unit_of_measurement) @@ -156,49 +158,41 @@ def device_class(self): @property def native_value(self) -> StateType | date | datetime | Decimal: - values = [ - self.get_valid_api_response_value_by_id(object_info.object_id, None) - for object_info in self.object_infos - ] - return self.entity_description.get_native_value(self, values) + return self.entity_description.get_native_value( + self, self.get_valid_api_responses() + ) @cached_property def native_unit_of_measurement(self): if native_unit_of_measurement := super().native_unit_of_measurement: return native_unit_of_measurement - if self.options: - return None - return self.object_infos[0].unit -class RctPowerFaultSensorEntity(RctPowerSensorEntity): - @property - def fault_bitmasks(self): - return [ - self.get_valid_api_response_value_by_id(object_info.object_id, 0) - for object_info in self.object_infos - ] +class RctPowerBitfieldSensorEntity(RctPowerSensorEntity): + @cached_property + def native_unit_of_measurement(self) -> str | None: + return None @property - def native_value(self): - fault_bitmasks = self.fault_bitmasks - - if all(isinstance(bitmask, int) for bitmask in fault_bitmasks): - return "{0:b}{1:b}{2:b}{3:b}".format(*fault_bitmasks) + def device_class(self): + """Return the device class of the sensor.""" + if device_class := super().device_class: + return device_class - return None + if self.options: + return SensorDeviceClass.ENUM - @cached_property - def native_unit_of_measurement(self): return None @property def extra_state_attributes(self): return { **(super().extra_state_attributes), - "fault_bitmasks": self.fault_bitmasks, + "bitfield": get_api_response_values_as_bitfield( + self, self.get_valid_api_responses() + ), } @@ -221,6 +215,15 @@ class RctPowerSensorEntityDescription( ] = get_first_api_response_value_as_state +@dataclass(frozen=True, kw_only=True) +class RctPowerBitfieldSensorEntityDescription( + RctPowerEntityDescription, SensorEntityDescription +): + get_native_value: Callable[ + [RctPowerSensorEntity, list[Optional[ApiResponseValue]]], StateType + ] = get_api_response_values_as_bitfield + + def slugify_entity_name(name: str): return name.replace(".", "_").replace("[", "_").replace("]", "_").replace("?", "_") diff --git a/custom_components/rct_power/lib/state_helpers.py b/custom_components/rct_power/lib/state_helpers.py index 6319b47..e94f2f3 100644 --- a/custom_components/rct_power/lib/state_helpers.py +++ b/custom_components/rct_power/lib/state_helpers.py @@ -1,14 +1,14 @@ -from typing import Literal, Optional +from typing import get_args +from typing import Literal +from typing import Optional from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.typing import StateType from .api import ApiResponseValue -from .const import ( - FREQUENCY_STATE_DECIMAL_DIGITS, - NUMERIC_STATE_DECIMAL_DIGITS, - BatteryStatusFlag, -) +from .const import BatteryStatusFlag +from .const import FREQUENCY_STATE_DECIMAL_DIGITS +from .const import NUMERIC_STATE_DECIMAL_DIGITS def get_first_api_response_value_as_state( @@ -75,8 +75,8 @@ def sum_api_response_values_as_state( # # Battery status # - -BatteryStatus = Literal["normal", "calibrating", "balancing"] +BatteryStatus = Literal["normal", "calibrating", "balancing", "other"] +available_battery_status: list[BatteryStatus] = list(get_args(BatteryStatus)) def get_api_response_value_as_battery_status( @@ -91,10 +91,10 @@ def get_api_response_value_as_battery_status( return "calibrating" case BatteryStatusFlag.balancing: return "balancing" - case _: + case BatteryStatusFlag.normal: return "normal" - - return None + case _: + return "other" def get_first_api_response_value_as_battery_status( @@ -106,3 +106,17 @@ def get_first_api_response_value_as_battery_status( return get_api_response_value_as_battery_status(entity, firstValue) case _: return None + + +# +# Bitfield +# + + +def get_api_response_values_as_bitfield( + entity: SensorEntity, + values: list[Optional[ApiResponseValue]], +) -> StateType: + return "".join(f"{value:b}" for value in values if isinstance(value, int)) + # if all(isinstance(bitmask, int) for bitmask in values): + # return "{0:b}{1:b}{2:b}{3:b}".format(*values) diff --git a/custom_components/rct_power/sensor.py b/custom_components/rct_power/sensor.py index d4ee44d..c83ce3a 100644 --- a/custom_components/rct_power/sensor.py +++ b/custom_components/rct_power/sensor.py @@ -1,20 +1,17 @@ """Sensor platform for RCT Power.""" -from typing import Callable, List +from typing import Callable +from typing import List from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from .lib.context import RctPowerContext -from .lib.entities import ( - battery_sensor_entity_descriptions, - fault_sensor_entity_descriptions, - inverter_sensor_entity_descriptions, -) -from .lib.entity import ( - RctPowerFaultSensorEntity, - RctPowerSensorEntity, -) +from .lib.entities import battery_sensor_entity_descriptions +from .lib.entities import bitfield_sensor_entity_descriptions +from .lib.entities import inverter_sensor_entity_descriptions +from .lib.entity import RctPowerBitfieldSensorEntity +from .lib.entity import RctPowerSensorEntity async def async_setup_entry( @@ -44,19 +41,19 @@ async def async_setup_entry( for entity_description in inverter_sensor_entity_descriptions ] - fault_sensor_entities = [ - RctPowerFaultSensorEntity( + bitfield_sensor_entities = [ + RctPowerBitfieldSensorEntity( coordinators=list(context.update_coordinators.values()), config_entry=entry, entity_description=entity_description, ) - for entity_description in fault_sensor_entity_descriptions + for entity_description in bitfield_sensor_entity_descriptions ] async_add_entities( [ *battery_sensor_entities, *inverter_sensor_entities, - *fault_sensor_entities, + *bitfield_sensor_entities, ] )