mirror of
https://github.com/home-assistant/core.git
synced 2025-11-15 22:10:09 +00:00
Use dpcode_wrapper in tuya binary sensor platform (#156247)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -15,11 +15,11 @@ from homeassistant.const import EntityCategory
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util.json import json_loads
|
|
||||||
|
|
||||||
from . import TuyaConfigEntry
|
from . import TuyaConfigEntry
|
||||||
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
|
from .models import DPCodeBitmapBitWrapper, DPCodeBooleanWrapper, DPCodeWrapper
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -366,20 +366,48 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_bitmap_bit_mask(
|
class _CustomDPCodeWrapper(DPCodeWrapper):
|
||||||
device: CustomerDevice, dpcode: str, bitmap_key: str | None
|
"""Custom DPCode Wrapper to check for values in a set."""
|
||||||
) -> int | None:
|
|
||||||
"""Get the bit mask for a given bitmap description."""
|
_valid_values: set[bool | float | int | str]
|
||||||
if (
|
|
||||||
bitmap_key is None
|
def __init__(
|
||||||
or (status_range := device.status_range.get(dpcode)) is None
|
self, dpcode: str, valid_values: set[bool | float | int | str]
|
||||||
or status_range.type != DPType.BITMAP
|
) -> None:
|
||||||
or not isinstance(bitmap_values := json_loads(status_range.values), dict)
|
"""Init CustomDPCodeBooleanWrapper."""
|
||||||
or not isinstance(bitmap_labels := bitmap_values.get("label"), list)
|
super().__init__(dpcode)
|
||||||
or bitmap_key not in bitmap_labels
|
self._valid_values = valid_values
|
||||||
):
|
|
||||||
|
def read_device_status(self, device: CustomerDevice) -> bool | None:
|
||||||
|
"""Read the device value for the dpcode."""
|
||||||
|
if (raw_value := self._read_device_status_raw(device)) is None:
|
||||||
return None
|
return None
|
||||||
return bitmap_labels.index(bitmap_key)
|
return raw_value in self._valid_values
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dpcode_wrapper(
|
||||||
|
device: CustomerDevice,
|
||||||
|
description: TuyaBinarySensorEntityDescription,
|
||||||
|
) -> DPCodeWrapper | None:
|
||||||
|
"""Get DPCode wrapper for an entity description."""
|
||||||
|
dpcode = description.dpcode or description.key
|
||||||
|
if description.bitmap_key is not None:
|
||||||
|
return DPCodeBitmapBitWrapper.find_dpcode(
|
||||||
|
device, dpcode, bitmap_key=description.bitmap_key
|
||||||
|
)
|
||||||
|
|
||||||
|
if bool_type := DPCodeBooleanWrapper.find_dpcode(device, dpcode):
|
||||||
|
return bool_type
|
||||||
|
|
||||||
|
# Legacy / compatibility
|
||||||
|
if dpcode not in device.status:
|
||||||
|
return None
|
||||||
|
return _CustomDPCodeWrapper(
|
||||||
|
dpcode,
|
||||||
|
description.on_value
|
||||||
|
if isinstance(description.on_value, set)
|
||||||
|
else {description.on_value},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -397,24 +425,10 @@ async def async_setup_entry(
|
|||||||
for device_id in device_ids:
|
for device_id in device_ids:
|
||||||
device = manager.device_map[device_id]
|
device = manager.device_map[device_id]
|
||||||
if descriptions := BINARY_SENSORS.get(device.category):
|
if descriptions := BINARY_SENSORS.get(device.category):
|
||||||
for description in descriptions:
|
entities.extend(
|
||||||
dpcode = description.dpcode or description.key
|
TuyaBinarySensorEntity(device, manager, description, dpcode_wrapper)
|
||||||
if dpcode in device.status:
|
for description in descriptions
|
||||||
mask = _get_bitmap_bit_mask(
|
if (dpcode_wrapper := _get_dpcode_wrapper(device, description))
|
||||||
device, dpcode, description.bitmap_key
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
description.bitmap_key is None # Regular binary sensor
|
|
||||||
or mask is not None # Bitmap sensor with valid mask
|
|
||||||
):
|
|
||||||
entities.append(
|
|
||||||
TuyaBinarySensorEntity(
|
|
||||||
device,
|
|
||||||
manager,
|
|
||||||
description,
|
|
||||||
mask,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
@@ -436,26 +450,15 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity):
|
|||||||
device: CustomerDevice,
|
device: CustomerDevice,
|
||||||
device_manager: Manager,
|
device_manager: Manager,
|
||||||
description: TuyaBinarySensorEntityDescription,
|
description: TuyaBinarySensorEntityDescription,
|
||||||
bit_mask: int | None = None,
|
dpcode_wrapper: DPCodeWrapper,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init Tuya binary sensor."""
|
"""Init Tuya binary sensor."""
|
||||||
super().__init__(device, device_manager)
|
super().__init__(device, device_manager)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||||
self._bit_mask = bit_mask
|
self._dpcode_wrapper = dpcode_wrapper
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool | None:
|
||||||
"""Return true if sensor is on."""
|
"""Return true if sensor is on."""
|
||||||
dpcode = self.entity_description.dpcode or self.entity_description.key
|
return self._dpcode_wrapper.read_device_status(self.device)
|
||||||
if dpcode not in self.device.status:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self._bit_mask is not None:
|
|
||||||
# For bitmap sensors, check the specific bit mask
|
|
||||||
return (self.device.status[dpcode] & (1 << self._bit_mask)) != 0
|
|
||||||
|
|
||||||
if isinstance(self.entity_description.on_value, set):
|
|
||||||
return self.device.status[dpcode] in self.entity_description.on_value
|
|
||||||
|
|
||||||
return self.device.status[dpcode] == self.entity_description.on_value
|
|
||||||
|
|||||||
@@ -101,6 +101,20 @@ class IntegerTypeData(TypeInformation):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BitmapTypeInformation(TypeInformation):
|
||||||
|
"""Bitmap type information."""
|
||||||
|
|
||||||
|
label: list[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
|
||||||
|
"""Load JSON string and return a BitmapTypeInformation object."""
|
||||||
|
if not (parsed := json.loads(data)):
|
||||||
|
return None
|
||||||
|
return cls(dpcode, **parsed)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnumTypeData(TypeInformation):
|
class EnumTypeData(TypeInformation):
|
||||||
"""Enum Type Data."""
|
"""Enum Type Data."""
|
||||||
@@ -116,6 +130,7 @@ class EnumTypeData(TypeInformation):
|
|||||||
|
|
||||||
|
|
||||||
_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
|
_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
|
||||||
|
DPType.BITMAP: BitmapTypeInformation,
|
||||||
DPType.BOOLEAN: TypeInformation,
|
DPType.BOOLEAN: TypeInformation,
|
||||||
DPType.ENUM: EnumTypeData,
|
DPType.ENUM: EnumTypeData,
|
||||||
DPType.INTEGER: IntegerTypeData,
|
DPType.INTEGER: IntegerTypeData,
|
||||||
@@ -147,13 +162,13 @@ class DPCodeWrapper(ABC):
|
|||||||
The raw device status is converted to a Home Assistant value.
|
The raw device status is converted to a Home Assistant value.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
|
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
|
||||||
"""Convert a Home Assistant value back to a raw device value.
|
"""Convert a Home Assistant value back to a raw device value.
|
||||||
|
|
||||||
This is called by `get_update_command` to prepare the value for sending
|
This is called by `get_update_command` to prepare the value for sending
|
||||||
back to the device, and should be implemented in concrete classes.
|
back to the device, and should be implemented in concrete classes if needed.
|
||||||
"""
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_update_command(self, device: CustomerDevice, value: Any) -> dict[str, Any]:
|
def get_update_command(self, device: CustomerDevice, value: Any) -> dict[str, Any]:
|
||||||
"""Get the update command for the dpcode.
|
"""Get the update command for the dpcode.
|
||||||
@@ -275,6 +290,48 @@ class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DPCodeBitmapBitWrapper(DPCodeWrapper):
|
||||||
|
"""Simple wrapper for a specific bit in bitmap values."""
|
||||||
|
|
||||||
|
def __init__(self, dpcode: str, mask: int) -> None:
|
||||||
|
"""Init DPCodeBitmapWrapper."""
|
||||||
|
super().__init__(dpcode)
|
||||||
|
self._mask = mask
|
||||||
|
|
||||||
|
def read_device_status(self, device: CustomerDevice) -> bool | None:
|
||||||
|
"""Read the device value for the dpcode."""
|
||||||
|
if (raw_value := self._read_device_status_raw(device)) is None:
|
||||||
|
return None
|
||||||
|
return (raw_value & (1 << self._mask)) != 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_dpcode(
|
||||||
|
cls,
|
||||||
|
device: CustomerDevice,
|
||||||
|
dpcodes: str | DPCode | tuple[DPCode, ...],
|
||||||
|
*,
|
||||||
|
bitmap_key: str,
|
||||||
|
) -> Self | None:
|
||||||
|
"""Find and return a DPCodeBitmapBitWrapper for the given DP codes."""
|
||||||
|
if (
|
||||||
|
type_information := find_dpcode(device, dpcodes, dptype=DPType.BITMAP)
|
||||||
|
) and bitmap_key in type_information.label:
|
||||||
|
return cls(
|
||||||
|
type_information.dpcode, type_information.label.index(bitmap_key)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def find_dpcode(
|
||||||
|
device: CustomerDevice,
|
||||||
|
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||||
|
*,
|
||||||
|
prefer_function: bool = False,
|
||||||
|
dptype: Literal[DPType.BITMAP],
|
||||||
|
) -> BitmapTypeInformation | None: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def find_dpcode(
|
def find_dpcode(
|
||||||
device: CustomerDevice,
|
device: CustomerDevice,
|
||||||
|
|||||||
Reference in New Issue
Block a user