Files
rikku-home-assistant/custom_components/localtuya/sensor.py
T
2025-04-21 21:03:30 -04:00

167 lines
5.2 KiB
Python

"""Platform to present any Tuya DP as a sensor."""
import logging
import base64
from functools import partial
from .config_flow import col_to_select
import voluptuous as vol
from homeassistant.components.sensor import (
DEVICE_CLASSES_SCHEMA,
DOMAIN,
STATE_CLASSES_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
Platform,
STATE_UNKNOWN,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfPower,
)
from homeassistant.helpers import entity_registry as er
from .entity import LocalTuyaEntity, async_setup_entry
from .const import CONF_SCALING, CONF_STATE_CLASS
_LOGGER = logging.getLogger(__name__)
DEFAULT_PRECISION = 2
ATTR_POWER = "power"
ATTR_VOLTAGE = "voltage"
ATTR_CURRENT = "current"
MAP_UOM = {
ATTR_CURRENT: UnitOfElectricCurrent.AMPERE,
ATTR_VOLTAGE: UnitOfElectricPotential.VOLT,
ATTR_POWER: UnitOfPower.KILO_WATT,
}
def flow_schema(dps):
"""Return schema used in config flow."""
return {
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): col_to_select(
[sc.value for sc in SensorStateClass]
),
vol.Optional(CONF_SCALING): vol.All(
vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0)
),
}
class LocalTuyaSensor(LocalTuyaEntity, SensorEntity):
"""Representation of a Tuya sensor."""
def __init__(
self,
device,
config_entry,
sensorid,
**kwargs,
):
"""Initialize the Tuya sensor."""
super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs)
self._state = None
self._has_sub_entities = False
self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
@property
def native_value(self):
"""Return sensor state."""
return self._state
@property
def state_class(self) -> str | None:
"""Return state class."""
return getattr(self, "_attr_state_class", self._config.get(CONF_STATE_CLASS))
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return getattr(
self,
"_attr_native_unit_of_measurement",
self._config.get(CONF_UNIT_OF_MEASUREMENT),
)
def status_updated(self):
"""Device status was updated."""
state = self.dp_value(self._dp_id)
if self.is_base64(state):
if not self._has_sub_entities:
self.hass.add_job(self.__create_sub_sensors())
if (sub_sensor := getattr(self, "_attr_sub_sensor", None)) and (
sub_state := self.decode_base64(state).get(sub_sensor)
):
self._state = sub_state
else:
self._state = state
else:
self._state = self.scale(state)
def status_restored(self, stored_state) -> None:
super().status_restored(stored_state)
if (last_state := self._last_state) and self.is_base64(last_state):
self._status.update({self._dp_id: last_state})
# No need to restore state for a sensor
async def restore_state_when_connected(self):
"""Do nothing for a sensor."""
return
def is_base64(self, data):
"""Return if the data is valid Tuya raw Base64 encoded data."""
return (
(data and isinstance(data, str))
and len(data) >= 12
and len(data) % 2 == 0
and data.endswith("=")
)
def decode_base64(self, data):
"""Decode data base64 such as DPS phase_a."""
buf = base64.b64decode(data)
voltage = (buf[1] | buf[0] << 8) / 10
current = (buf[4] | buf[3] << 8) / 1000
power = (buf[7] | buf[6] << 8) / 1000
return {ATTR_VOLTAGE: voltage, ATTR_CURRENT: current, ATTR_POWER: power}
async def __create_sub_sensors(self):
"""Create sub entities for voltage, current and power and hide this parent sensor."""
sub_entities = []
for sensor in (ATTR_CURRENT, ATTR_POWER, ATTR_VOLTAGE):
sub_entity = LocalTuyaSensor(
self._device, self._device_config.as_dict(), self._dp_id
)
setattr(sub_entity, "_attr_sub_sensor", sensor)
setattr(sub_entity, "_attr_unique_id", f"{self.unique_id}_{sensor}")
setattr(sub_entity, "_attr_name", f"{self.name} {sensor.capitalize()}")
setattr(sub_entity, "_attr_device_class", SensorDeviceClass(sensor))
setattr(sub_entity, "_attr_state_class", SensorStateClass.MEASUREMENT)
setattr(sub_entity, "_attr_native_unit_of_measurement", MAP_UOM[sensor])
sub_entities.append(sub_entity)
# Sub entities shouldn't have add entities attr.
if sub_entities and self.componet_add_entities:
self._has_sub_entities = True
self.componet_add_entities(sub_entities)
er.async_get(self.hass).async_update_entity(
self.entity_id, hidden_by=er.RegistryEntryHider.INTEGRATION
)
async_setup_entry = partial(async_setup_entry, DOMAIN, LocalTuyaSensor, flow_schema)