Sfoglia il codice sorgente

Add the ability to mark dps as optional.

Issue #179.

A number of configs with known dps that were commented out because of not being consistently available have had this option enabled.

Should resolve #187 as well, without adding a specific config.
Jason Rumney 3 anni fa
parent
commit
6b339f4370

+ 32 - 14
custom_components/tuya_local/devices/README.md

@@ -77,7 +77,7 @@ secondary entities, so only basic functionality is implemented.
 
 
 ### `deprecated`
 ### `deprecated`
 
 
-*Optional, deprecated*
+*Deprecated, DO NOT USE for new devices.*
 
 
 This is used to mark an entity as deprecated.  This is mainly
 This is used to mark an entity as deprecated.  This is mainly
 for older devices that were implemented when only climate devices were
 for older devices that were implemented when only climate devices were
@@ -162,12 +162,21 @@ to use a secondary entity for that.
 
 
 ### `readonly`
 ### `readonly`
 
 
-*Optional.*
+*Optional, default false.*
 
 
 A boolean setting to mark attributes as readonly. If not specified, the
 A boolean setting to mark attributes as readonly. If not specified, the
 default is `false`.  If set to `true`, the attributes will be reported
 default is `false`.  If set to `true`, the attributes will be reported
 to Home Assistant, but no functionality for setting them will be exposed.
 to Home Assistant, but no functionality for setting them will be exposed.
 
 
+### `optional`
+
+*Optional, default false.*
+
+A boolean setting to mark attributes as optional.  This allows a device to be
+matched even if it is not sending the dp at the time when adding a new device.
+It can also be used to match a range of devices that have variations in the extra
+attributes that are sent.
+
 ### `mapping`
 ### `mapping`
 
 
 *Optional.*
 *Optional.*
@@ -182,7 +191,7 @@ defined in their own section below.
 
 
 ### `hidden`
 ### `hidden`
 
 
-*Optional.*
+*Optional, default false.*
 This can be used to define DPs that do not directly expose Home Assistant
 This can be used to define DPs that do not directly expose Home Assistant
 attributes.  When set to **true**, no attribute will be sent. A `name` should
 attributes.  When set to **true**, no attribute will be sent. A `name` should
 still be specified and the attribute can be referenced as a `constraint`
 still be specified and the attribute can be referenced as a `constraint`
@@ -198,7 +207,7 @@ is set to Eco.
 
 
 ### `range`
 ### `range`
 
 
-*Optional.*
+*Optional, may be required in some contexts, may have defaults in others.*
 
 
 For integer attributes that are not readonly, a range can be set with `min`
 For integer attributes that are not readonly, a range can be set with `min`
 and `max` values that will limit the values that the user can enter in the
 and `max` values that will limit the values that the user can enter in the
@@ -206,7 +215,7 @@ Home Assistant UI.  This can also be set in a `mapping` or `conditions` block.
 
 
 ### `unit`
 ### `unit`
 
 
-*Optional. default="C" for temperature dps on climate devices, None for sensors.*
+*Optional, default="C" for temperature dps on climate devices.*
 
 
 For temperature dps, some devices will use Fahrenhiet.  This needs to be
 For temperature dps, some devices will use Fahrenhiet.  This needs to be
 indicated back to HomeAssistant by defining `unit` as "F".  For sensor 
 indicated back to HomeAssistant by defining `unit` as "F".  For sensor 
@@ -216,7 +225,7 @@ equivalents, other units are currently ASCII so can be easily entered directly).
 
 
 ### `class`
 ### `class`
 
 
-*Optional.  default=None.*
+*Optional.*
 
 
 For sensors, this sets the state class of the sensor (measurement, total
 For sensors, this sets the state class of the sensor (measurement, total
 or total_increasing)
 or total_increasing)
@@ -224,7 +233,7 @@ or total_increasing)
 
 
 ### `format`
 ### `format`
 
 
-*Optional. default=None*
+*Optional.*
 
 
 For base64 and hex types, this specifies how to decode the binary data (after hex or base64 decoding).
 For base64 and hex types, this specifies how to decode the binary data (after hex or base64 decoding).
 This is a container field, the contents of which should be a list consisting of `name`, `bytes` and `range` fields.  `range` is as described above.  `bytes` is the number of bytes for the field, which can be `1`, `2`, or `4`.  `name` is a name for the field, which will have special handling depending on
 This is a container field, the contents of which should be a list consisting of `name`, `bytes` and `range` fields.  `range` is as described above.  `bytes` is the number of bytes for the field, which can be `1`, `2`, or `4`.  `name` is a name for the field, which will have special handling depending on
@@ -266,7 +275,7 @@ level to override all values, but I can't imagine a useful purpose for that.
 
 
 ### `scale`
 ### `scale`
 
 
-*Optional, default=1*
+*Optional, default=1.*
 
 
 This can be used in an `integer` dp mapping to scale the values.  For example
 This can be used in an `integer` dp mapping to scale the values.  For example
 some climate devices represent the temperature as an integer in tenths of
 some climate devices represent the temperature as an integer in tenths of
@@ -277,7 +286,7 @@ of 0.03.
 
 
 ###`invert`
 ###`invert`
 
 
-*Optional, default=False*
+*Optional, default=False.*
 
 
 This can be used in an `integer` dp mapping to invert the range.  For example,
 This can be used in an `integer` dp mapping to invert the range.  For example,
 some cover devices have an opposite idea of which end of the percentage scale open
 some cover devices have an opposite idea of which end of the percentage scale open
@@ -286,7 +295,7 @@ must also be specified for the dp.
 
 
 ### `step`
 ### `step`
 
 
-*Optional, default=1*
+*Optional, default=1.*
 
 
 This can be used in an `integer` dp mapping to make values jump by a specific
 This can be used in an `integer` dp mapping to make values jump by a specific
 step.  It can also be set in a conditions block so that the steps change only
 step.  It can also be set in a conditions block so that the steps change only
@@ -346,7 +355,7 @@ have a mapping that mirrors the value of the configuration dp.
 
 
 ### `invalid`
 ### `invalid`
 
 
-*Optional. Boolean, default false.*
+*Optional, default false.*
 
 
 Invalid set to true allows an attribute to temporarily be set read-only in
 Invalid set to true allows an attribute to temporarily be set read-only in
 some conditions.  Rather than passing requests to set the attribute through
 some conditions.  Rather than passing requests to set the attribute through
@@ -360,14 +369,14 @@ control when the preset is in sleep mode (since sleep mode should force low).
 
 
 ### `constraint`
 ### `constraint`
 
 
-*Optional. Always paired with `conditions`.*
+*Optional, always paired with `conditions`.*
 
 
 If a rule depends on an attribute other than the current one, then `constraint`
 If a rule depends on an attribute other than the current one, then `constraint`
 can be used to specify the element that `conditions` applies to.
 can be used to specify the element that `conditions` applies to.
 
 
 ### `conditions`
 ### `conditions`
 
 
-*Optional. Always paired with `constraint.`*
+*Optional, always paired with `constraint.`*
 
 
 Conditions defines a list of rules that are applied based on the `constraint`
 Conditions defines a list of rules that are applied based on the `constraint`
 attribute. The contents are the same as Mapping Rules, but `dps_val` applies
 attribute. The contents are the same as Mapping Rules, but `dps_val` applies
@@ -464,7 +473,16 @@ Humidifer can also cover dehumidifiers (use class to specify which).
    If the light mixes in color modes in the same dp, **color_mode** should be used instead.
    If the light mixes in color modes in the same dp, **color_mode** should be used instead.
 
 
 ### lock
 ### lock
-- **lock** (required, boolean): a dp to control the lock state: true = locked, false = unlocked
+- **lock** (optional, boolean): a dp to control the lock state: true = locked, false = unlocked
+- **unlock_fingerprint** (optional, integer): a dp to identify the fingerprint used to unlock the lock.
+- **unlock_password** (optional, integer): a dp to identify the password used to unlock the lock.
+- **unlock_temp_pwd** (optional, integer): a dp to identify the temporary password used to unlock the lock.
+- **unlock_dynamic_pwd** (optional, integer): a dp to identify the dynamic password used to unlock the lock.
+- **unlock_card** (optional, integer): a dp to identify the card used to unlock the lock.
+- **unlock_app** (optional, integer): a dp to identify the app used to unlock the lock.
+- **request_unlock** (optional, integer): a dp to signal that a request has been made to unlock, the value should indicate the time remaining for approval.
+- **approve_unlock** (optional, boolean): a dp to unlock the lock in response to a request.
+- **jammed** (optional, boolean): a dp to signal that the lock is jammed.
 
 
 ### number
 ### number
 - **value** (required, number): a dp to control the number that is set.
 - **value** (required, number): a dp to control the number that is set.

+ 70 - 60
custom_components/tuya_local/devices/bresser_weather_station.yaml

@@ -19,9 +19,10 @@ primary_entity:
           - scale: 10
           - scale: 10
         class: measurement
         class: measurement
         readonly: true
         readonly: true
-#    - id: 22
-#      type: string
-#      name: fault_type
+    - id: 22
+      type: string
+      name: fault_type
+      optional: true
     - id: 58
     - id: 58
       type: string
       type: string
       name: wind_direct
       name: wind_direct
@@ -134,27 +135,30 @@ secondary_entities:
     dps:
     dps:
       - id: 30
       - id: 30
         <<: *battery
         <<: *battery
-#  - entity: binary_sensor
-#    name: Battery Ch1
-#    class: battery
-#    category: diagnostic
-#    dps:
-#      - id: 31
-#        <<: *battery
-#  - entity: binary_sensor
-#    name: Battery Ch2
-#    class: battery
-#    category: diagnostic
-#    dps:
-#      - id: 32
-#        <<: *battery
-#  - entity: binary_sensor
-#    name: Battery Ch3
-#    class: battery
-#    category: diagnostic
-#    dps:
-#      - id: 33
-#        <<: *battery
+  - entity: binary_sensor
+    name: Battery Ch1
+    class: battery
+    category: diagnostic
+    dps:
+      - id: 31
+        optional: true
+        <<: *battery
+  - entity: binary_sensor
+    name: Battery Ch2
+    class: battery
+    category: diagnostic
+    dps:
+      - id: 32
+        optional: true
+        <<: *battery
+  - entity: binary_sensor
+    name: Battery Ch3
+    class: battery
+    category: diagnostic
+    dps:
+      - id: 33
+        optional: true
+        <<: *battery
   - entity: sensor
   - entity: sensor
     name: Temperature
     name: Temperature
     class: temperature
     class: temperature
@@ -167,42 +171,48 @@ secondary_entities:
     dps:
     dps:
       - id: 39
       - id: 39
         <<: *humidity
         <<: *humidity
-#  - entity: sensor
-#    name: Temperature Ch1
-#    class: temperature
-#    dps:
-#      - id: 40
-#        <<: *temperature
-#  - entity: sensor
-#    name: Humidity Ch1
-#    class: humidity
-#    dps:
-#      - id: 41
-#        <<: *humidity
-#  - entity: sensor
-#    name: Temperature Ch2
-#    class: temperature
-#    dps:
-#      - id: 42
-#        <<: *temperature
-#  - entity: sensor
-#    name: Humidity Ch2
-#    class: humidity
-#    dps:
-#      - id: 43
-#        <<: *humidity
-#  - entity: sensor
-#    name: Temperature Ch3
-#    class: temperature
-#    dps:
-#      - id: 44
-#        <<: *temperature
-#  - entity: sensor
-#    name: Humidity Ch3
-#    class: humidity
-#    dps:
-#      - id: 45
-#        <<: *humidity
+  - entity: sensor
+    name: Temperature Ch1
+    class: temperature
+    dps:
+      - id: 40
+        optional: true
+        <<: *temperature
+  - entity: sensor
+    name: Humidity Ch1
+    class: humidity
+    dps:
+      - id: 41
+        optional: true
+        <<: *humidity
+  - entity: sensor
+    name: Temperature Ch2
+    class: temperature
+    dps:
+      - id: 42
+        optional: true
+        <<: *temperature
+  - entity: sensor
+    name: Humidity Ch2
+    class: humidity
+    dps:
+      - id: 43
+        optional: true
+        <<: *humidity
+  - entity: sensor
+    name: Temperature Ch3
+    class: temperature
+    dps:
+      - id: 44
+        optional: true
+        <<: *temperature
+  - entity: sensor
+    name: Humidity Ch3
+    class: humidity
+    dps:
+      - id: 45
+        optional: true
+        <<: *humidity
   - entity: sensor
   - entity: sensor
     name: Pressure
     name: Pressure
     class: pressure
     class: pressure

+ 10 - 8
custom_components/tuya_local/devices/kyvol_e30_vacuum.yaml

@@ -85,14 +85,16 @@ primary_entity:
           value: gentle
           value: gentle
         - dps_val: "4"
         - dps_val: "4"
           value: closed
           value: closed
-#    - id: 15
-#      type: string
-#      name: clean_record
-#      readonly: true
-#    - id: 16
-#      type: integer
-#      name: clean_area
-#      unit: m2
+    - id: 15
+      type: string
+      name: clean_record
+      readonly: true
+      optional: true
+    - id: 16
+      type: integer
+      name: clean_area
+      unit: m2
+      optional: true
     - id: 18
     - id: 18
       type: bitfield
       type: bitfield
       name: error
       name: error

+ 13 - 15
custom_components/tuya_local/devices/lefant_m213_vacuum.yaml

@@ -65,11 +65,11 @@ primary_entity:
     - id: 13
     - id: 13
       type: boolean
       type: boolean
       name: locate
       name: locate
-# Listed at iot.tuya.com, but does not seem to be returned by default
-#    - id: 15
-#      type: string
-#      name: clean_record
-#      readonly: true
+    - id: 15
+      type: string
+      name: clean_record
+      readonly: true
+      optional: true
     - id: 18
     - id: 18
       type: bitfield
       type: bitfield
       name: error
       name: error
@@ -107,16 +107,14 @@ primary_entity:
     - id: 104
     - id: 104
       type: integer
       type: integer
       name: unknown_104
       name: unknown_104
-# dp 106 returned only by Lefant M213 and M213S, not by APOSEN A550
-# seems to be some encoded ASCII descriptor related to charging status
-#    - id: 106
-#      type: string
-#      name: unknown_106
-# dp 108 returned only sometimes?
-# seems to be some encoded ASCII descriptor of battery voltage
-#    - id: 108
-#      type: string
-#      name: unknown_108
+    - id: 106
+      type: string
+      name: charge_status
+      optional: true
+    - id: 108
+      type: string
+      name: battery_status
+      optional: true
 secondary_entities:
 secondary_entities:
   - entity: sensor
   - entity: sensor
     name: Clean Area
     name: Clean Area

+ 12 - 10
custom_components/tuya_local/devices/orion_smart_lock.yaml

@@ -35,16 +35,18 @@ primary_entity:
     - id: 15
     - id: 15
       type: integer
       type: integer
       name: unlock_app
       name: unlock_app
-# Suspect the following are not always reported, as they are large hex fields
-#    - id: 25
-#      type: hex
-#      name: fingers_enrolled
-#    - id: 26
-#      type: hex
-#      name: passwords_enrolled
-#    - id: 27
-#      type: hex
-#      name: cards_enrolled
+    - id: 25
+      type: hex
+      name: fingers_enrolled
+      optional: true
+    - id: 26
+      type: hex
+      name: passwords_enrolled
+      optional: true
+    - id: 27
+      type: hex
+      name: cards_enrolled
+      optional: true
 secondary_entities:
 secondary_entities:
   - entity: sensor
   - entity: sensor
     name: alert
     name: alert

+ 23 - 17
custom_components/tuya_local/devices/rgbcw_lightbulb.yaml

@@ -31,6 +31,7 @@ primary_entity:
     - id: 23
     - id: 23
       name: color_temp
       name: color_temp
       type: integer
       type: integer
+      optional: true
       range:
       range:
         min: 0
         min: 0
         max: 1000
         max: 1000
@@ -39,6 +40,7 @@ primary_entity:
     - id: 24
     - id: 24
       name: rgbhsv
       name: rgbhsv
       type: hex
       type: hex
+      optional: true
       format:
       format:
         - name: h
         - name: h
           bytes: 2
           bytes: 2
@@ -58,23 +60,27 @@ primary_entity:
     - id: 25
     - id: 25
       name: scene_data
       name: scene_data
       type: hex
       type: hex
-#  The following were removed because they aren't always present,
-#  and anyway they are complex hex data fields that we cannot handle.
-#   - id: 27
-#     name: music_data
-#     type: hex
-#   - id: 28
-#     name: control_data
-#     type: hex
-#   - id: 30
-#     name: rhythm_mode
-#     type: hex
-#   - id: 31
-#     name: sleep_mode
-#     type: hex
-#   - id: 32
-#     name: wakeup_mode
-#     type: hex
+      optional: true
+    - id: 27
+      name: music_data
+      type: hex
+      optional: true
+    - id: 28
+      name: control_data
+      type: hex
+      optional: true
+    - id: 30
+      name: rhythm_mode
+      type: hex
+      optional: true
+    - id: 31
+      name: sleep_mode
+      type: hex
+      optional: true
+    - id: 32
+      name: wakeup_mode
+      type: hex
+      optional: true
 secondary_entities:
 secondary_entities:
   - entity: number
   - entity: number
     name: Timer
     name: Timer

+ 13 - 4
custom_components/tuya_local/helpers/device_config.py

@@ -112,12 +112,16 @@ class TuyaDeviceConfig:
     def matches(self, dps):
     def matches(self, dps):
         """Determine if this device matches the provided dps map."""
         """Determine if this device matches the provided dps map."""
         for d in self.primary_entity.dps():
         for d in self.primary_entity.dps():
-            if d.id not in dps.keys() or not _typematch(d.type, dps[d.id]):
+            if (d.id not in dps.keys() and not d.optional) or (
+                d.id in dps.keys() and not _typematch(d.type, dps[d.id])
+            ):
                 return False
                 return False
 
 
         for dev in self.secondary_entities():
         for dev in self.secondary_entities():
             for d in dev.dps():
             for d in dev.dps():
-                if d.id not in dps.keys() or not _typematch(d.type, dps[d.id]):
+                if (d.id not in dps.keys() and not d.optional) or (
+                    d.id in dps.keys() and not _typematch(d.type, dps[d.id])
+                ):
                     return False
                     return False
         _LOGGER.debug("Matched config for %s", self.name)
         _LOGGER.debug("Matched config for %s", self.name)
         return True
         return True
@@ -136,8 +140,8 @@ class TuyaDeviceConfig:
             True if all dps in entity could be matched to dps, False otherwise
             True if all dps in entity could be matched to dps, False otherwise
         """
         """
         for d in entity.dps():
         for d in entity.dps():
-            if (d.id not in keys and d.id not in matched) or not _typematch(
-                d.type, dps[d.id]
+            if (d.id not in keys and d.id not in matched and not d.optional) or (
+                (d.id in keys or d.id in matched) and not _typematch(d.type, dps[d.id])
             ):
             ):
                 return False
                 return False
             if d.id in keys:
             if d.id in keys:
@@ -290,6 +294,10 @@ class TuyaDpsConfig:
     def name(self):
     def name(self):
         return self._config["name"]
         return self._config["name"]
 
 
+    @property
+    def optional(self):
+        return self._config.get("optional", False)
+
     @property
     @property
     def format(self):
     def format(self):
         fmt = self._config.get("format")
         fmt = self._config.get("format")
@@ -485,6 +493,7 @@ class TuyaDpsConfig:
             except ValueError:
             except ValueError:
                 self.stringify = False
                 self.stringify = False
         else:
         else:
+
             self.stringify = False
             self.stringify = False
 
 
         result = value
         result = value

+ 3 - 1
custom_components/tuya_local/helpers/mixin.py

@@ -73,7 +73,9 @@ class TuyaLocalEntity:
         """Get additional attributes that the platform itself does not support."""
         """Get additional attributes that the platform itself does not support."""
         attr = {}
         attr = {}
         for a in self._attr_dps:
         for a in self._attr_dps:
-            attr[a.name] = a.get_value(self._device)
+            value = a.get_value(self._device)
+            if value is not None or not a.optional:
+                attr[a.name] = value
         return attr
         return attr
 
 
     async def async_update(self):
     async def async_update(self):

+ 1 - 1
tests/devices/base_device_tests.py

@@ -47,7 +47,7 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
         self.addCleanup(device_patcher.stop)
         self.addCleanup(device_patcher.stop)
         self.mock_device = device_patcher.start()
         self.mock_device = device_patcher.start()
         self.dps = payload.copy()
         self.dps = payload.copy()
-        self.mock_device.get_property.side_effect = lambda id: self.dps[id]
+        self.mock_device.get_property.side_effect = lambda id: self.dps.get(id)
         cfg = TuyaDeviceConfig(config_file)
         cfg = TuyaDeviceConfig(config_file)
         self.conf_type = cfg.legacy_type
         self.conf_type = cfg.legacy_type
         type(self.mock_device).has_returned_state = PropertyMock(return_value=True)
         type(self.mock_device).has_returned_state = PropertyMock(return_value=True)