Explorar el Código

Add the binary_sensor platform.

- Add defrost and tank sensors for Goldair Dehumidifier for testing it.
Jason Rumney hace 4 años
padre
commit
434a221eee

+ 50 - 0
custom_components/tuya_local/binary_sensor.py

@@ -0,0 +1,50 @@
+"""
+Setup for different kinds of Tuya Binary sensors
+"""
+import logging
+
+from . import DOMAIN
+from .const import (
+    CONF_DEVICE_ID,
+    CONF_TYPE,
+)
+from .generic.binary_sensor import TuyaLocalBinarySensor
+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 == "binary_sensor" and discovery_info.get(ecfg.config_id, False):
+        data[ecfg.config_id] = TuyaLocalBinarySensor(device, ecfg)
+        sensors.append(data[ecfg.config_id])
+        if ecfg.deprecated:
+            _LOGGER.warning(ecfg.deprecation_message)
+        _LOGGER.debug(f"Adding binary_sensor for {discovery_info[ecfg.config_id]}")
+
+    for ecfg in cfg.secondary_entities():
+        if ecfg.entity == "binary_sensor" and discovery_info.get(ecfg.config_id, False):
+            data[ecfg.config_id] = TuyaLocalBinarySensor(device, ecfg)
+            sensors.append(data[ecfg.config_id])
+            if ecfg.deprecated:
+                _LOGGER.warning(ecfg.deprecation_message)
+            _LOGGER.debug(f"Adding binary_sensor for {discovery_info[ecfg.config_id]}")
+    if not sensors:
+        raise ValueError(
+            f"{device.name} does not support use as a binary_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)

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

@@ -273,3 +273,21 @@ secondary_entities:
         class: measurement
         unit: "%"
         readonly: true
+  - entity: binary_sensor
+    name: Tank
+    class: problem
+    dps:
+      - id: 11
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 8
+            value: true
+          - value: false
+  - entity: binary_sensor
+    name: defrost
+    class: cold
+    dps:
+      - id: 105
+        type: boolean
+        name: sensor

+ 39 - 0
custom_components/tuya_local/generic/binary_sensor.py

@@ -0,0 +1,39 @@
+"""
+Platform to read Tuya binary sensors.
+"""
+from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity
+
+from ..device import TuyaLocalDevice
+from ..helpers.device_config import TuyaEntityConfig
+from ..helpers.mixin import TuyaLocalEntity
+
+
+class TuyaLocalBinarySensor(TuyaLocalEntity, BinarySensorEntity):
+    """Representation of a Tuya Binary 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
+        """
+        dps_map = self._init_begin(device, config)
+        self._sensor_dps = dps_map.pop("sensor")
+        if self._sensor_dps is None:
+            raise AttributeError(f"{config.name} is missing a sensor dps")
+        self._init_end(dps_map)
+
+    @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 is_on(self):
+        """Return true if the binary sensor is on."""
+        return self._sensor_dps.get_value(self._device)

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

@@ -23,12 +23,18 @@
 		"description": "Choose a name for this device, and which entities will be enabled",
 		"data": {
 		    "name": "Name",
-		    "humidifier": "Include a humidifier entity",
-		    "fan": "Include a fan entitiy",
+		    "binary_sensor": "Include a binary sensor entity",
 		    "climate": "Include a climate entity",
+		    "fan": "Include a fan entitiy",
+		    "humidifier": "Include a humidifier entity",
 		    "light": "Include a light entity",
 		    "lock": "Include a lock entity",
+		    "number": "Include a number entity",
+		    "select": "Include a select entity",
+		    "sensor": "Include a sensor entity",
 		    "switch": "Include a switch entity",
+		    "binary_sensor_tank": "Include tank as a binary_sensor entity.",
+		    "binary_sensor_defrost": "Include defrost as a binary_sensor entity.",
 		    "climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
 		    "fan_intensity": "Include intensity as a fan entitiy",
 		    "light_aq_indicator": "Include AQ indicator as a light entity",
@@ -69,12 +75,18 @@
             "data": {
 		"host": "IP address or hostname",
 		"local_key": "Local key",
-		"humidifier": "Include a humidifier entity",
-		"fan": "Include a fan entitiy",
+		"binary_sensor": "Include a binary sensor entity",
 		"climate": "Include a climate entity",
+		"fan": "Include a fan entitiy",
+		"humidifier": "Include a humidifier entity",
 		"light": "Include a light entity",
 		"lock": "Include a lock entity",
+		"number": "Include a number entity",
+		"select": "Include a select entity",
+		"sensor": "Include a sensor entity",
 		"switch": "Include a switch entity",
+		"binary_sensor_tank": "Include tank as a binary_sensor entity.",
+		"binary_sensor_defrost": "Include defrost as a binary_sensor entity.",
 		"climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
 		"fan_intensity": "Include intensity as a fan entitiy",
 		"light_aq_indicator": "Include AQ indicator as a light entity",

+ 1 - 0
hacs.json

@@ -2,6 +2,7 @@
   "name": "Tuya Local",
   "render_readme": true,
     "domains": [
+	"binary_sensor",
 	"climate",
 	"fan",
 	"humidifier",

+ 2 - 0
tests/devices/base_device_tests.py

@@ -7,6 +7,7 @@ from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.components.switch import DEVICE_CLASS_SWITCH
 from homeassistant.const import STATE_UNAVAILABLE
 
+from custom_components.tuya_local.generic.binary_sensor import TuyaLocalBinarySensor
 from custom_components.tuya_local.generic.climate import TuyaLocalClimate
 from custom_components.tuya_local.generic.fan import TuyaLocalFan
 from custom_components.tuya_local.generic.humidifier import TuyaLocalHumidifier
@@ -25,6 +26,7 @@ from custom_components.tuya_local.helpers.device_config import (
 from ..helpers import assert_device_properties_set
 
 DEVICE_TYPES = {
+    "binary_sensor": TuyaLocalBinarySensor,
     "climate": TuyaLocalClimate,
     "fan": TuyaLocalFan,
     "humidifier": TuyaLocalHumidifier,

+ 22 - 1
tests/devices/test_goldair_dehumidifier.py

@@ -1,6 +1,10 @@
 from unittest import skip
 from unittest.mock import ANY
 
+from homeassistant.components.binary_sensor import (
+    DEVICE_CLASS_COLD,
+    DEVICE_CLASS_PROBLEM,
+)
 from homeassistant.components.climate.const import (
     FAN_HIGH,
     FAN_LOW,
@@ -11,7 +15,6 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_HUMIDITY,
 )
 from homeassistant.components.light import COLOR_MODE_ONOFF
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.const import (
     DEVICE_CLASS_HUMIDITY,
     DEVICE_CLASS_TEMPERATURE,
@@ -70,6 +73,8 @@ class TestGoldairDehumidifier(
         self.setUpBasicSwitch(AIRCLEAN_DPS, self.entities.get("switch_air_clean"))
         self.temperature = self.entities.get("sensor_current_temperature")
         self.humidity = self.entities.get("sensor_current_humidity")
+        self.tank = self.entities.get("binary_sensor_tank")
+        self.defrost = self.entities.get("binary_sensor_defrost")
 
     def test_supported_features(self):
         self.assertEqual(
@@ -626,3 +631,19 @@ class TestGoldairDehumidifier(
     def test_sensor_device_class(self):
         self.assertEqual(self.temperature.device_class, DEVICE_CLASS_TEMPERATURE)
         self.assertEqual(self.humidity.device_class, DEVICE_CLASS_HUMIDITY)
+
+    def test_binary_sensor_device_class(self):
+        self.assertEqual(self.tank.device_class, DEVICE_CLASS_PROBLEM)
+        self.assertEqual(self.defrost.device_class, DEVICE_CLASS_COLD)
+
+    def test_binary_sensor_is_on(self):
+        self.dps[ERROR_DPS] = 0
+        self.dps[DEFROST_DPS] = False
+        self.assertFalse(self.tank.is_on)
+        self.assertFalse(self.defrost.is_on)
+        self.dps[ERROR_DPS] = 8
+        self.dps[DEFROST_DPS] = True
+        self.assertTrue(self.tank.is_on)
+        self.assertTrue(self.defrost.is_on)
+        self.dps[ERROR_DPS] = 1
+        self.assertFalse(self.tank.is_on)

+ 77 - 0
tests/test_binary_sensor.py

@@ -0,0 +1,77 @@
+"""Tests for the binary_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.binary_sensor import TuyaLocalBinarySensor
+from custom_components.tuya_local.binary_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,
+            "binary_sensor_tank": True,
+            "binary_sensor_defrost": 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"]["binary_sensor_tank"]) == TuyaLocalBinarySensor
+    )
+    m_add_entities.assert_called_once()
+
+
+async def test_init_entry_fails_if_device_has_no_binary_sensor(hass):
+    """Test initialisation when device has no matching entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: "mirabella_genio_usb", CONF_DEVICE_ID: "dummy"},
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {"device": m_device},
+    }
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+async def test_init_entry_fails_if_config_is_missing(hass):
+    """Test initialisation when device has no matching entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={CONF_TYPE: "non_existing", CONF_DEVICE_ID: "dummy"},
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {"device": m_device},
+    }
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()