Просмотр исходного кода

Entity availability: add option to hide unavailable entities.

The use of available is split between two purposes:

1. To support multiple devices with a single config, based on feature
   flags or availability of optional dps.
2. To dynamically make entities available based on user settable options.

In the first case, we want HA to hide the entities, as they will never
be available for the device. But in the second case, they will
sometimes be available, so we do not want to hide them.

Add a third option for the "hidden" entity attribute which determines
if entities be hidden when "unavailable"

May help with recent reports where users are saying their devices
became unavailable after upgrade, as in most cases we should not be
hiding them just because no data has been received yet (probably HA is
only checking this at startup and will not later enable the entity if
it becomes available).
Jason Rumney 1 год назад
Родитель
Сommit
8ab5ab405b

+ 6 - 1
custom_components/tuya_local/devices/README.md

@@ -119,9 +119,14 @@ sense with one UI mode, then this is provided to handle those cases.
 
 ### `hidden`
 
-*Optional, default=false*
+*Optional, true/unavailable, default=false*
 
 If `hidden` is `true`, then the entity will be disabled by default.
+If `hidden` is `unavailable`, then the entity will be disabled by default if
+the entity's `available` dp indicates it is unavailable. This may not work
+correctly if the device has not returned data yet when HA checks
+for this at startup.
+
 This can be used with advanced config or diagnostic entities that general
 users will not be interested in. To use such entities, the user must explicitly
 enable them after adding the device to Home Assistant.

+ 1 - 0
custom_components/tuya_local/devices/beca_bht002_thermostat_c.yaml

@@ -69,6 +69,7 @@ entities:
   - entity: sensor
     name: External temperature
     class: temperature
+    hidden: unavailable
     dps:
       - id: 102
         type: integer

+ 1 - 0
custom_components/tuya_local/devices/duux_blizzard_portable_aircon.yaml

@@ -112,6 +112,7 @@ entities:
   - entity: switch
     translation_key: ionizer
     category: config
+    hidden: unavailable
     dps:
       - id: 7
         type: boolean

+ 8 - 0
custom_components/tuya_local/devices/fairland_x20_poolheatpump.yaml

@@ -129,6 +129,7 @@ entities:
         name: fault_code_2
   - entity: sensor
     category: diagnostic
+    hidden: unavailable
     class: power
     dps:
       - id: 112
@@ -140,3 +141,10 @@ entities:
         name: available
         type: boolean
         optional: true
+        mapping:
+          - dps_val: null
+            value: true
+            constraint: sensor
+            conditions:
+              - dps_val: null
+                value: false

+ 10 - 0
custom_components/tuya_local/devices/fisher_summer_air_conditioner.yaml

@@ -159,6 +159,7 @@ entities:
         optional: true
   - entity: sensor  # no sensor in Fisher Summer AC, returns 0
     class: pm25
+    hidden: unavailable
     dps:
       - id: 101
         name: sensor
@@ -174,6 +175,7 @@ entities:
   - entity: select
     name: Sleep
     category: config
+    hidden: unavailable
     dps:
       - id: 105
         name: option
@@ -216,6 +218,7 @@ entities:
   - entity: select
     name: Horizontal sweep
     category: config
+    hidden: unavailable
     dps:
       - id: 114
         name: option
@@ -248,6 +251,7 @@ entities:
   - entity: select
     name: Energy saving
     category: config
+    hidden: unavailable
     dps:
       - id: 119
         name: option
@@ -273,6 +277,7 @@ entities:
   - entity: select
     name: Generator mode
     category: config
+    hidden: unavailable
     dps:
       - id: 120
         name: option
@@ -297,6 +302,7 @@ entities:
           - value: false
   - entity: sensor
     translation_key: air_quality
+    hidden: unavailable
     dps:
       - id: 125
         name: sensor
@@ -312,6 +318,7 @@ entities:
   - entity: select
     name: Set vertical direction
     category: config
+    hidden: unavailable
     dps:
       - id: 126
         name: option
@@ -340,6 +347,7 @@ entities:
   - entity: select
     name: Set horizontal direction  # Tuya JSON phrasing: Freeze Horizontal
     category: config
+    hidden: unavailable
     dps:
       - id: 127
         name: option
@@ -414,6 +422,7 @@ entities:
   - entity: binary_sensor
     name: Dirty filter
     class: problem
+    hidden: unavailable
     category: diagnostic
     dps:
       - id: 131
@@ -430,6 +439,7 @@ entities:
   - entity: select
     name: Hot cold wind
     category: config
+    hidden: unavailable
     dps:
       - id: 132
         name: option

+ 1 - 0
custom_components/tuya_local/devices/goldair_portable_airconditioner.yaml

@@ -138,6 +138,7 @@ entities:
             available: support_vswing
   - entity: switch
     translation_key: ionizer
+    hidden: unavailable
     # no mention of ionizer/anion in manual but unit still beeps
     # when the value is toggled
     dps:

+ 10 - 0
custom_components/tuya_local/devices/plikc_neve_thermostat.yaml

@@ -229,6 +229,7 @@ entities:
   - entity: number
     name: Maximum temperature
     category: config
+    hidden: unavailable
     icon: "mdi:thermometer-chevron-up"
     dps:
       - id: 19
@@ -252,6 +253,7 @@ entities:
   - entity: number
     name: Minimum temperature
     category: config
+    hidden: unavailable
     icon: "mdi:thermometer-chevron-down"
     dps:
       - id: 26
@@ -274,6 +276,7 @@ entities:
           - value: true
   - entity: sensor
     name: Time period
+    hidden: unavailable
     category: diagnostic
     dps:
       - id: 105
@@ -299,6 +302,7 @@ entities:
   - entity: sensor
     class: power  # marked as electricity, but range of 0-6000 suggests W
     category: diagnostic
+    hidden: unavailable
     dps:
       - id: 106
         type: integer
@@ -317,6 +321,7 @@ entities:
   - entity: select
     name: System mode
     icon: "mdi:help-box-multiple"
+    hidden: unavailable
     category: config
     dps:
       - id: 108
@@ -343,6 +348,7 @@ entities:
   - entity: switch
     translation_key: anti_frost
     category: config
+    hidden: unavailable
     dps:
       - id: 109
         type: boolean
@@ -358,6 +364,7 @@ entities:
           - value: true
   - entity: switch
     name: PIN protect
+    hidden: unavailable
     icon: "mdi:dialpad"
     category: config
     dps:
@@ -376,6 +383,7 @@ entities:
   - entity: select
     name: "Off mode"
     icon: "mdi:power-standby"
+    hidden: unavailable
     category: config
     dps:
       - id: 111
@@ -398,6 +406,7 @@ entities:
   - entity: number
     name: PIN code
     category: config
+    hidden: unavailable
     icon: "mdi:dialpad"
     dps:
       - id: 116
@@ -419,6 +428,7 @@ entities:
     name: Partial key lock
     translation_key: child_lock
     category: config
+    hidden: unavailable
     dps:
       - id: 118
         type: boolean

+ 1 - 0
custom_components/tuya_local/devices/simple_switch_timerv2.yaml

@@ -36,6 +36,7 @@ entities:
             step: 60
   - entity: select
     translation_key: initial_state
+    hidden: unavailable
     category: config
     dps:
       - id: 38

+ 9 - 0
custom_components/tuya_local/devices/smartplugv1.yaml

@@ -48,7 +48,16 @@ entities:
     name: Overcurrent alarm
     class: problem
     category: diagnostic
+    hidden: unavailable
     dps:
+      - id: 7
+        type: boolean
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 7
         type: boolean
         name: sensor

+ 36 - 0
custom_components/tuya_local/devices/smartplugv2_energy.yaml

@@ -126,8 +126,17 @@ entities:
         optional: true
   - entity: select
     category: config
+    hidden: unavailable
     translation_key: initial_state
     dps:
+      - id: 38
+        type: string
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 38
         type: string
         name: option
@@ -142,8 +151,17 @@ entities:
   - entity: switch
     name: Overcharge cutoff
     category: config
+    hidden: unavailable
     icon: "mdi:battery-charging"
     dps:
+      - id: 46
+        type: boolean
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 46
         type: boolean
         name: switch
@@ -154,8 +172,17 @@ entities:
   - entity: select
     name: Light
     icon: "mdi:circle-double"
+    hidden: unavailable
     category: config
     dps:
+      - id: 39
+        type: string
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 39
         type: string
         name: option
@@ -171,8 +198,17 @@ entities:
             value: "On"
   - entity: lock
     translation_key: child_lock
+    hidden: unavailable
     category: config
     dps:
+      - id: 40
+        type: boolean
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 40
         type: boolean
         name: lock

+ 28 - 1
custom_components/tuya_local/devices/smartplugv2_energyv2.yaml

@@ -138,7 +138,16 @@ entities:
   - entity: switch
     name: Overcharge protection
     category: config
+    hidden: unavailable
     dps:
+      - id: 39
+        type: boolean
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 39
         type: boolean
         name: switch
@@ -154,7 +163,16 @@ entities:
     name: Light mode
     icon: "mdi:television-ambient-light"
     category: config
+    hidden: unavailable
     dps:
+      - id: 40
+        type: string
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 40
         type: string
         name: option
@@ -169,10 +187,19 @@ entities:
   - entity: binary_sensor
     name: Online
     class: connectivity
+    hidden: unavailable
     category: diagnostic
     dps:
       - id: 66
-        type: boolean
+        type: string
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
+      - id: 66
+        type: string
         optional: true
         name: sensor
         mapping:

+ 27 - 0
custom_components/tuya_local/devices/smartplugv2_energyv3.yaml

@@ -145,7 +145,16 @@ entities:
   - entity: lock
     translation_key: child_lock
     category: config
+    hidden: unavailable
     dps:
+      - id: 41
+        type: boolean
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 41
         type: boolean
         name: lock
@@ -170,15 +179,33 @@ entities:
   - entity: switch
     name: Overcharge protection
     category: config
+    hidden: unavailable
     dps:
+      - id: 39
+        type: boolean
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 39
         type: boolean
         name: switch
         optional: true
   - entity: sensor
     class: temperature
+    hidden: unavailable
     category: diagnostic
     dps:
+      - id: 47
+        type: integer
+        optional: true
+        name: available
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true
       - id: 47
         type: integer
         optional: true

+ 4 - 1
custom_components/tuya_local/entity.py

@@ -13,6 +13,9 @@ from homeassistant.helpers.entity import EntityCategory
 
 _LOGGER = logging.getLogger(__name__)
 
+# These attributes should not be included in the extra state attributes
+BLACKLISTED_ATTRIBUTES = ["state", "available"]
+
 
 class TuyaLocalEntity:
     """Common functions for all entity types."""
@@ -30,7 +33,7 @@ class TuyaLocalEntity:
 
     def _init_end(self, dps):
         for d in dps.values():
-            if not d.hidden:
+            if not d.hidden and d.name not in BLACKLISTED_ATTRIBUTES:
                 self._attr_dps.append(d)
 
     @property

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

@@ -357,11 +357,17 @@ class TuyaEntityConfig:
 
     def enabled_by_default(self, device):
         """Return whether this entity should be disabled by default."""
-        return (
-            not self._config.get("hidden", False)
-            and not self.deprecated
-            and self.available(device)
-        )
+        hidden = self._config.get("hidden", False)
+        if hidden == "unavailable":
+            avail_dp = self.find_dps("available")
+            if not avail_dp:
+                _LOGGER.warning(
+                    "Entity %s / %s has hidden: unavailable but no available dp defined",
+                    self._device.config_type,
+                    self.name,
+                )
+            hidden = not self.available(device)
+        return not hidden and not self.deprecated
 
 
 class TuyaDpsConfig:

+ 3 - 2
tests/devices/base_device_tests.py

@@ -96,10 +96,11 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
     def test_config_matched(self):
         for cfg in possible_matches(self.dps):
             if cfg.legacy_type == self.conf_type:
+                quality = cfg.match_quality(self.dps)
                 self.assertEqual(
-                    cfg.match_quality(self.dps),
+                    quality,
                     100.0,
-                    msg=f"{self.conf_type} is an imperfect match",
+                    msg=f"{self.conf_type} is an imperfect match at {quality}%",
                 )
                 return
         self.fail()

+ 0 - 93
tests/devices/test_smartplugv1.py

@@ -1,93 +0,0 @@
-"""Tests for the switch entity."""
-
-from homeassistant.components.binary_sensor import BinarySensorDeviceClass
-from homeassistant.components.number import NumberDeviceClass
-from homeassistant.components.sensor import SensorDeviceClass
-from homeassistant.components.switch import SwitchDeviceClass
-from homeassistant.const import (
-    UnitOfElectricCurrent,
-    UnitOfElectricPotential,
-    UnitOfPower,
-    UnitOfTime,
-)
-
-from ..const import KOGAN_SOCKET_PAYLOAD
-from ..mixins.binary_sensor import BasicBinarySensorTests
-from ..mixins.number import BasicNumberTests
-from ..mixins.sensor import MultiSensorTests
-from ..mixins.switch import SwitchableTests
-from .base_device_tests import TuyaDeviceTestCase
-
-SWITCH_DPS = "1"
-TIMER_DPS = "2"
-CURRENT_DPS = "4"
-POWER_DPS = "5"
-VOLTAGE_DPS = "6"
-OVERCURRENT_DPS = "7"
-
-
-class TestKoganSwitch(
-    BasicBinarySensorTests,
-    BasicNumberTests,
-    MultiSensorTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-):
-    __test__ = True
-
-    def setUp(self):
-        self.setUpForConfig("smartplugv1.yaml", KOGAN_SOCKET_PAYLOAD)
-        self.subject = self.entities.get("switch_outlet")
-        self.setUpSwitchable(SWITCH_DPS, self.subject)
-        self.setUpBasicBinarySensor(
-            OVERCURRENT_DPS,
-            self.entities.get("binary_sensor_overcurrent_alarm"),
-            device_class=BinarySensorDeviceClass.PROBLEM,
-        )
-        self.setUpBasicNumber(
-            TIMER_DPS,
-            self.entities.get("number_timer"),
-            max=1440.0,
-            unit=UnitOfTime.MINUTES,
-            device_class=NumberDeviceClass.DURATION,
-            scale=60,
-        )
-        self.setUpMultiSensors(
-            [
-                {
-                    "name": "sensor_voltage",
-                    "dps": VOLTAGE_DPS,
-                    "unit": UnitOfElectricPotential.VOLT,
-                    "device_class": SensorDeviceClass.VOLTAGE,
-                    "state_class": "measurement",
-                    "testdata": (2300, 230.0),
-                },
-                {
-                    "name": "sensor_current",
-                    "dps": CURRENT_DPS,
-                    "unit": UnitOfElectricCurrent.MILLIAMPERE,
-                    "device_class": SensorDeviceClass.CURRENT,
-                    "state_class": "measurement",
-                },
-                {
-                    "name": "sensor_power",
-                    "dps": POWER_DPS,
-                    "unit": UnitOfPower.WATT,
-                    "device_class": SensorDeviceClass.POWER,
-                    "state_class": "measurement",
-                    "testdata": (1234, 123.4),
-                },
-            ]
-        )
-        self.mark_secondary(
-            [
-                "binary_sensor_overcurrent_alarm",
-                "number_timer",
-                "sensor_current",
-                "sensor_power",
-                "sensor_voltage",
-            ]
-        )
-
-    def test_device_class_is_outlet(self):
-        self.assertEqual(self.subject.device_class, SwitchDeviceClass.OUTLET)

+ 6 - 0
tests/devices/test_smartplugv2_energy.py

@@ -178,3 +178,9 @@ class TestSwitchV2Energy(
             self.basicBSensor.extra_state_attributes,
             {"fault_code": 2},
         )
+
+    def test_available(self):
+        self.dps[INITIAL_DPS] = None
+        self.assertFalse(self.basicSelect.available)
+        self.dps[INITIAL_DPS] = "on"
+        self.assertTrue(self.basicSelect.available)

+ 1 - 1
tests/test_device_config.py

@@ -158,7 +158,7 @@ ENTITY_SCHEMA = vol.Schema(
         vol.Optional("icon_priority"): int,
         vol.Optional("deprecated"): str,
         vol.Optional("mode"): vol.In(["box", "slider"]),
-        vol.Optional("hidden"): True,
+        vol.Optional("hidden"): vol.In([True, "unavailable"]),
         vol.Required("dps"): [DP_SCHEMA],
     }
 )