From 0dc4fc2939e37748e478856b879934df5fd92d6d Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Thu, 13 Apr 2023 12:27:45 +0000 Subject: [PATCH 1/3] status entities for battery calibration & balancing --- custom_components/rct_power/lib/entities.py | 40 ++++++++++++++- custom_components/rct_power/lib/entity.py | 42 +++++++++++++++- .../rct_power/lib/state_helpers.py | 50 ++++++++++++++++++- custom_components/rct_power/sensor.py | 12 +++++ 4 files changed, 139 insertions(+), 5 deletions(-) diff --git a/custom_components/rct_power/lib/entities.py b/custom_components/rct_power/lib/entities.py index 47967fb..d554fbd 100644 --- a/custom_components/rct_power/lib/entities.py +++ b/custom_components/rct_power/lib/entities.py @@ -5,10 +5,16 @@ from rctclient.registry import REGISTRY from .device_info_helpers import get_battery_device_info, get_inverter_device_info -from .entity import EntityUpdatePriority, RctPowerSensorEntityDescription +from .entity import ( + EntityUpdatePriority, + RctPowerSensorEntityDescription, + RctPowerBinarySensorEntityDescription, +) from .state_helpers import ( get_first_api_reponse_value_as_absolute_state, sum_api_response_values_as_state, + get_battery_calibration_status, + get_battery_balancing_status, ) @@ -191,6 +197,31 @@ def get_matching_names(expression: str): ), ] +battery_binary_sensor_entity_descriptions: List[ + RctPowerBinarySensorEntityDescription +] = [ + RctPowerBinarySensorEntityDescription( + get_device_info=get_battery_device_info, + key="battery.bat_status.calibrating", + object_names=["battery.bat_status"], + name="Battery Is Calibrating", + update_priority=EntityUpdatePriority.FREQUENT, + get_native_binary_value=get_battery_calibration_status, + ), + RctPowerBinarySensorEntityDescription( + get_device_info=get_battery_device_info, + key="battery.bat_status.balancing", + # 'battery.status2' is not required here, this is just a hack + # so that this entity's generated id doesn't conflict with + # "battery.bat_status.calibrating" + # changing the id generation scheme would break existing installations + object_names=["battery.bat_status", "battery.status2"], + name="Battery Is Balancing", + update_priority=EntityUpdatePriority.FREQUENT, + get_native_binary_value=get_battery_balancing_status, + ), +] + inverter_sensor_entity_descriptions: List[RctPowerSensorEntityDescription] = [ RctPowerSensorEntityDescription( get_device_info=get_inverter_device_info, @@ -717,4 +748,9 @@ def get_matching_names(expression: str): *fault_sensor_entity_descriptions, ] -all_entity_descriptions = [*sensor_entity_descriptions] +binary_sensor_entity_descriptions = [*battery_binary_sensor_entity_descriptions] + +all_entity_descriptions = [ + *sensor_entity_descriptions, + *binary_sensor_entity_descriptions, +] diff --git a/custom_components/rct_power/lib/entity.py b/custom_components/rct_power/lib/entity.py index 79d96c0..8be8848 100644 --- a/custom_components/rct_power/lib/entity.py +++ b/custom_components/rct_power/lib/entity.py @@ -2,6 +2,10 @@ from typing import Any, Callable, Dict, List, Optional from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.typing import StateType @@ -17,7 +21,10 @@ 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_first_api_response_value_as_state, + get_first_api_response_value_as_binary_state, +) from .update_coordinator import RctPowerDataUpdateCoordinator @@ -121,6 +128,30 @@ def device_info(self): return self.entity_description.get_device_info(self) +class RctPowerBinarySensorEntity(BinarySensorEntity, RctPowerEntity): + entity_description: "RctPowerBinarySensorEntityDescription" + + @property + def device_class(self): + """Return the device class of the sensor.""" + if device_class := super().device_class: + return device_class + + return None + + @property + def native_value(self): + values = [ + self.get_valid_api_response_value_by_id(object_id, None) + for object_id in self.object_ids + ] + return self.entity_description.get_native_binary_value(self, values) + + @property + def is_on(self): + return self.native_value + + class RctPowerSensorEntity(SensorEntity, RctPowerEntity): entity_description: "RctPowerSensorEntityDescription" @@ -198,6 +229,15 @@ def __post_init__(self): ] +@dataclass +class RctPowerBinarySensorEntityDescription( + RctPowerEntityDescription, BinarySensorEntityDescription +): + get_native_binary_value: Callable[ + [RctPowerBinarySensorEntity, list[Optional[ApiResponseValue]]], StateType + ] = get_first_api_response_value_as_binary_state + + @dataclass class RctPowerSensorEntityDescription( RctPowerEntityDescription, SensorEntityDescription diff --git a/custom_components/rct_power/lib/state_helpers.py b/custom_components/rct_power/lib/state_helpers.py index 4f1d67f..db0af37 100644 --- a/custom_components/rct_power/lib/state_helpers.py +++ b/custom_components/rct_power/lib/state_helpers.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import Optional, Union -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import Entity, SensorEntity +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.typing import StateType from .api import ApiResponseValue @@ -60,3 +61,48 @@ def sum_api_response_values_as_state( for value in values if isinstance(value, (int, float)) ) + + +def get_first_api_response_value_as_binary_state( + entity: BinarySensorEntity, + values: list[Optional[ApiResponseValue]], +) -> Union[None, bool]: + if len(values) <= 0: + return None + + return get_api_response_value_as_binary_state(entity=entity, value=values[0]) + + +def get_api_response_value_as_binary_state( + entity: BinarySensorEntity, + value: Optional[ApiResponseValue], +) -> Union[None, bool]: + if value is None: + return None + return bool(value) + + +def get_battery_calibration_status( + entity: BinarySensorEntity, + values: list[Optional[ApiResponseValue]], +) -> Union[None, bool]: + if len(values) <= 0: + return None + value = values[0] + + if isinstance(value, int): + return value & 1032 != 0 + return None + + +def get_battery_balancing_status( + entity: BinarySensorEntity, + values: list[Optional[ApiResponseValue]], +) -> Union[None, bool]: + if len(values) <= 0: + return None + value = values[0] + + if isinstance(value, int): + return value & 2048 != 0 + return None diff --git a/custom_components/rct_power/sensor.py b/custom_components/rct_power/sensor.py index d4ee44d..ab27eb2 100644 --- a/custom_components/rct_power/sensor.py +++ b/custom_components/rct_power/sensor.py @@ -8,10 +8,12 @@ from .lib.context import RctPowerContext from .lib.entities import ( battery_sensor_entity_descriptions, + battery_binary_sensor_entity_descriptions, fault_sensor_entity_descriptions, inverter_sensor_entity_descriptions, ) from .lib.entity import ( + RctPowerBinarySensorEntity, RctPowerFaultSensorEntity, RctPowerSensorEntity, ) @@ -35,6 +37,15 @@ async def async_setup_entry( for entity_description in battery_sensor_entity_descriptions ] + battery_binary_sensor_entities = [ + RctPowerBinarySensorEntity( + coordinators=list(context.update_coordinators.values()), + config_entry=entry, + entity_description=entity_description, + ) + for entity_description in battery_binary_sensor_entity_descriptions + ] + inverter_sensor_entities = [ RctPowerSensorEntity( coordinators=list(context.update_coordinators.values()), @@ -56,6 +67,7 @@ async def async_setup_entry( async_add_entities( [ *battery_sensor_entities, + *battery_binary_sensor_entities, *inverter_sensor_entities, *fault_sensor_entities, ] From b73c0bd150030b8e81332d460bf371c8775b33cb Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Sun, 16 Apr 2023 16:57:25 +0000 Subject: [PATCH 2/3] use better names for entities --- custom_components/rct_power/lib/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/rct_power/lib/entities.py b/custom_components/rct_power/lib/entities.py index d554fbd..449b03d 100644 --- a/custom_components/rct_power/lib/entities.py +++ b/custom_components/rct_power/lib/entities.py @@ -204,7 +204,7 @@ def get_matching_names(expression: str): get_device_info=get_battery_device_info, key="battery.bat_status.calibrating", object_names=["battery.bat_status"], - name="Battery Is Calibrating", + name="Battery Calibration Active", update_priority=EntityUpdatePriority.FREQUENT, get_native_binary_value=get_battery_calibration_status, ), @@ -216,7 +216,7 @@ def get_matching_names(expression: str): # "battery.bat_status.calibrating" # changing the id generation scheme would break existing installations object_names=["battery.bat_status", "battery.status2"], - name="Battery Is Balancing", + name="Battery Balancing Active", update_priority=EntityUpdatePriority.FREQUENT, get_native_binary_value=get_battery_balancing_status, ), From 06ceef370498a154653ad6714240c5287b7c66ab Mon Sep 17 00:00:00 2001 From: Oliver Rahner Date: Mon, 17 Apr 2023 12:56:59 +0200 Subject: [PATCH 3/3] fix linter error --- custom_components/rct_power/lib/state_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/rct_power/lib/state_helpers.py b/custom_components/rct_power/lib/state_helpers.py index db0af37..c6481ae 100644 --- a/custom_components/rct_power/lib/state_helpers.py +++ b/custom_components/rct_power/lib/state_helpers.py @@ -1,6 +1,6 @@ from typing import Optional, Union -from homeassistant.components.sensor import Entity, SensorEntity +from homeassistant.components.sensor import SensorEntity from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.typing import StateType