Explorar el Código

Add sensor platform.
- add secondary entities for temperature and humidity sensors to goldair dehumidifier for testing.

Cleanup translations (sync between initial config and options).

Jason Rumney hace 4 años
padre
commit
d972428ae9

+ 20 - 0
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -253,3 +253,23 @@ secondary_entities:
       - id: 5
         type: boolean
         name: switch
+  - entity: sensor
+    name: Current Temperature
+    class: temperature
+    dps:
+      - id: 103
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        readonly: true
+  - entity: sensor
+    name: Current Humidity
+    class: humidity
+    dps:
+      - id: 104
+        type: integer
+        name: sensor
+        class: measurement
+        unit: "%"
+        readonly: true

+ 90 - 0
custom_components/tuya_local/generic/sensor.py

@@ -0,0 +1,90 @@
+"""
+Platform to read Tuya sensors.
+"""
+from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity, STATE_CLASSES
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+
+from ..device import TuyaLocalDevice
+from ..helpers.device_config import TuyaEntityConfig
+
+
+class TuyaLocalSensor(SensorEntity):
+    """Representation of a Tuya Sensor"""
+
+    def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
+        """
+        Initialise the sensor.
+        Args:
+            device (TuyaLocalDevice): the device API instance.
+            config (TuyaEntityConfig): the configuration for this entity
+        """
+        self._device = device
+        self._config = config
+        self._attr_dps = []
+        dps_map = {c.name: c for c in config.dps()}
+        self._sensor_dps = dps_map.pop("sensor")
+
+        if self._sensor_dps is None:
+            raise AttributeError(f"{config.name} is missing a sensor dps")
+
+        for d in dps_map.values():
+            if not d.hidden:
+                self._attr_dps.append(d)
+
+    @property
+    def should_poll(self):
+        """Return the polling state."""
+        return True
+
+    @property
+    def name(self):
+        """Return the name for this entity."""
+        return self._config.name(self._device.name)
+
+    @property
+    def unique_id(self):
+        """Return the unique id of the device."""
+        return self._config.unique_id(self._device.unique_id)
+
+    @property
+    def device_info(self):
+        """Return device information about the device."""
+        return self._device.device_info
+
+    @property
+    def device_class(self):
+        """Return the class of this device"""
+        dclass = self._config.device_class
+        if dclass in DEVICE_CLASSES:
+            return dclass
+        else:
+            return None
+
+    @property
+    def state_class(self):
+        """Return the state class of this entity"""
+        sclass = self._sensor_dps.state_class
+        if sclass in STATE_CLASSES:
+            return sclass
+        else:
+            return None
+
+    @property
+    def native_value(self):
+        """Return the value reported by the sensor"""
+        return self._sensor_dps.get_value(self._device)
+
+    @property
+    def native_unit_of_measurement(self):
+        """Return the unit for the sensor"""
+        unit = self._sensor_dps.unit
+        # Temperatures use Unicode characters, translate from simpler ASCII
+        if unit == "C":
+            unit = TEMP_CELSIUS
+        elif unit == "F":
+            unit = TEMP_FAHRENHEIT
+
+        return unit
+
+    async def async_update(self):
+        await self._device.async_refresh()

+ 5 - 0
custom_components/tuya_local/helpers/device_config.py

@@ -364,6 +364,11 @@ class TuyaDpsConfig:
     def unit(self):
         return self._config.get("unit")
 
+    @property
+    def state_class(self):
+        """The state class of this measurement."""
+        return self._config.get("class")
+
     def _find_map_for_dps(self, value):
         default = None
         for m in self._config.get("mapping", {}):

+ 1 - 1
custom_components/tuya_local/manifest.json

@@ -2,7 +2,7 @@
     "domain": "tuya_local",
     "iot_class": "local_polling",
     "name": "Tuya Local",
-    "version": "0.12.3",
+    "version": "0.13.0",
     "documentation": "https://github.com/make-all/tuya-local",
     "issue_tracker": "https://github.com/make-all/tuya-local/issues",
     "dependencies": [],

+ 49 - 0
custom_components/tuya_local/sensor.py

@@ -0,0 +1,49 @@
+"""
+Setup for different kinds of Tuya sensors
+"""
+import logging
+
+from . import DOMAIN
+from .const import (
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+)
+from .generic.sensor import TuyaLocalSensor
+from .helpers.device_config import get_config
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+    """Set up the sensor device according to it's type."""
+    data = hass.data[DOMAIN][discovery_info[CONF_DEVICE_ID]]
+    device = data["device"]
+    sensors = []
+
+    cfg = get_config(discovery_info[CONF_TYPE])
+    if cfg is None:
+        raise ValueError(f"No device config found for {discovery_info}")
+    ecfg = cfg.primary_entity
+    if ecfg.entity == "sensor" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalSensor(device, ecfg)
+        sensors.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding sensor for {discovery_info[ecfg.config_id]}")
+
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "sensor" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalSensor(device, ecfg)
+            sensors.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding sensor for {discovery_info[ecfg.config_id]}")
+
+    if not sensors:
+        raise ValueError(f"{device.name} does not support use as a sensor device.")
+    async_add_entities(sensors)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    config = {**config_entry.data, **config_entry.options}
+    await async_setup_platform(hass, {}, async_add_entities, config)

+ 7 - 4
custom_components/tuya_local/translations/en.json

@@ -35,6 +35,8 @@
 		    "light_display": "Include display as a light entity",
 		    "light_uv_sterilization": "Include UV sterilization as a light entitiy",
 		    "lock_child_lock": "Include child lock as a lock entity",
+		    "sensor_current_humidity": "Include current humidity as a sensor entity",
+		    "sensor_current_temperature": "Include current temperature as a sensor entity",
 		    "switch_air_clean": "Include air clean as a switch entity",
 		    "switch_ionizer": "Include ionizer as a switch entity",
 		    "switch_master": "Include master switch as a switch entity",
@@ -70,21 +72,22 @@
 		"lock": "Include a lock entity",
 		"switch": "Include a switch entity",
 		"climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
-		"fan_fan": "Include a fan entity",
 		"fan_intensity": "Include intensity as a fan entitiy",
-		"humidifier_humidifier": "Include a humidifier entity",
 		"light_aq_indicator": "Include AQ indicator as a light entity",
 		"light_display": "Include display as a light entity",
-		"light_light": "Include a light entity",
 		"light_uv_sterilization": "Include UV sterilization as a light entitiy",
 		"lock_child_lock": "Include child lock as a lock entity",
+		"sensor_current_humidity": "Include current humidity as a sensor entity",
+		"sensor_current_temperature": "Include current temperature as a sensor entity",
 		"switch_air_clean": "Include air clean as a switch entity",
 		"switch_ionizer": "Include ionizer as a switch entity",
 		"switch_master": "Include master switch as a switch entity",
 		"switch_open_window_detector": "Include open window detect as a switch entity",
+		"switch_outlet_1": "Include outlet 1 as a switch entity",
+		"switch_outlet_2": "Include outlet 2 as a switch entity",
 		"switch_sleep": "Include sleep mode as a switch entity",
 		"switch_sound": "Include sound mute as a switch entity",
-		    "switch_uv_sterilization": "Include UV sterilization as a switch"
+		"switch_uv_sterilization": "Include UV sterilization as a switch"
             }
 	}
     },

+ 1 - 1
hacs.json

@@ -1,7 +1,7 @@
 {
   "name": "Tuya Local",
   "render_readme": true,
-  "domains": ["climate", "fan", "humidifier", "light", "lock", "switch"],
+  "domains": ["climate", "fan", "humidifier", "light", "lock", "sensor", "switch"],
   "homeassistant": "2021.10.0",
   "iot_class": "Local Polling"
 }

+ 2 - 0
tests/devices/base_device_tests.py

@@ -7,6 +7,7 @@ from custom_components.tuya_local.generic.fan import TuyaLocalFan
 from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
 from custom_components.tuya_local.generic.light import TuyaLocalLight
 from custom_components.tuya_local.generic.lock import TuyaLocalLock
+from custom_components.tuya_local.generic.sensor import TuyaLocalSensor
 from custom_components.tuya_local.generic.switch import TuyaLocalSwitch
 
 from custom_components.tuya_local.helpers.device_config import (
@@ -21,6 +22,7 @@ DEVICE_TYPES = {
     "light": TuyaLocalLight,
     "lock": TuyaLocalLock,
     "switch": TuyaLocalSwitch,
+    "sensor": TuyaLocalSensor,
 }
 
 

+ 11 - 2
tests/devices/test_goldair_dehumidifier.py

@@ -12,7 +12,7 @@ from homeassistant.components.climate.const import (
 )
 from homeassistant.components.light import COLOR_MODE_ONOFF
 from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS
 
 from ..const import DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -51,6 +51,8 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
         self.light = self.entities.get("light_display")
         self.lock = self.entities.get("lock_child_lock")
         self.switch = self.entities.get("switch_air_clean")
+        self.temperature = self.entities.get("sensor_current_temperature")
+        self.humidity = self.entities.get("sensor_current_humidity")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -120,6 +122,7 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
     def test_current_humidity(self):
         self.dps[CURRENTHUMID_DPS] = 47
         self.assertEqual(self.climate.current_humidity, 47)
+        self.assertEqual(self.humidity.native_value, 47)
 
     def test_min_target_humidity(self):
         self.assertEqual(self.climate.min_humidity, 30)
@@ -224,7 +227,12 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
 
     def test_temperature_unit_returns_device_temperature_unit(self):
         self.assertEqual(
-            self.climate.temperature_unit, self.climate._device.temperature_unit
+            self.climate.temperature_unit,
+            self.climate._device.temperature_unit,
+        )
+        self.assertEqual(
+            self.temperature.native_unit_of_measurement,
+            TEMP_CELSIUS,
         )
 
     def test_minimum_target_temperature(self):
@@ -236,6 +244,7 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
     def test_current_temperature(self):
         self.dps[CURRENTTEMP_DPS] = 25
         self.assertEqual(self.climate.current_temperature, 25)
+        self.assertEqual(self.temperature.native_value, 25)
 
     def test_hvac_mode(self):
         self.dps[HVACMODE_DPS] = True

+ 38 - 0
tests/test_sensor.py

@@ -0,0 +1,38 @@
+"""Tests for the sensor entity."""
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+from unittest.mock import AsyncMock, Mock
+
+from custom_components.tuya_local.const import (
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+    DOMAIN,
+)
+from custom_components.tuya_local.generic.sensor import TuyaLocalSensor
+from custom_components.tuya_local.sensor import async_setup_entry
+
+
+async def test_init_entry(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_TYPE: "goldair_dehumidifier",
+            CONF_DEVICE_ID: "dummy",
+            "humidifier": False,
+            "sensor_current_temperature": True,
+            "sensor_current_humidity": False,
+        },
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {"device": m_device},
+    }
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert (
+        type(hass.data[DOMAIN]["dummy"]["sensor_current_temperature"])
+        == TuyaLocalSensor
+    )
+    m_add_entities.assert_called_once()