167 lines
5.2 KiB
Python
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)
|