Skip to content

Commit

Permalink
refactor to bitfield sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
weltenwort committed Jan 19, 2024
1 parent aad9e86 commit 6c58d19
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 82 deletions.
11 changes: 7 additions & 4 deletions custom_components/rct_power/lib/const.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
46 changes: 23 additions & 23 deletions custom_components/rct_power/lib/entities.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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] = [
Expand Down Expand Up @@ -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=[
Expand All @@ -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 = [
Expand Down
63 changes: 33 additions & 30 deletions custom_components/rct_power/lib/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -140,65 +139,60 @@ 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)

return None

@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()
),
}


Expand All @@ -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("?", "_")

Expand Down
36 changes: 25 additions & 11 deletions custom_components/rct_power/lib/state_helpers.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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)
25 changes: 11 additions & 14 deletions custom_components/rct_power/sensor.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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,
]
)

0 comments on commit 6c58d19

Please sign in to comment.