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

Orion Siren: use tone to turn on and off the siren.

- Use the tone attribute as the primary on/off for the siren, as that is the way it appears to work.
- Add support for a `default` value to be marked in a mapping.  Use this when siren.turn_on is called with no tone value and the siren is not already on.
- Treat "off" tone specially, use it to turn_off instead of handling in turn_on
- map "normal" to "off" in the Orion siren config, mark sound+light as default.

Issue #198
Jason Rumney 3 лет назад
Родитель
Сommit
85cc6bec44

+ 16 - 0
custom_components/tuya_local/devices/README.md

@@ -344,6 +344,16 @@ able to unset it.  Instead, this should be used with conditions, below, to
 make the behaviour dependent on another DP, such as disabling fan speed 
 control when the preset is in sleep mode (since sleep mode should force low).
 
+### `default`
+
+*Optional, default false.*
+
+Default set to true allows an attribute to be set as the default value.
+This is used by some entities when an argument is not provided to a service call
+but the attribute is required to be set to function correctly.
+An example is the siren entity which uses the tone attribute to turn on and
+off the siren, but when turn_on is called without any argument, it needs to
+pick a defaulttone to use to turn on the siren.
 
 ### `constraint`
 
@@ -496,3 +506,9 @@ Humidifer can also cover dehumidifiers (use class to specify which).
     These are additional commands that are not part of **status**. They can be sent as general commands from HA.
 - **error** (optional, bitfield): a dp that reports error status.
     As this is mapped to a single "fault" state, you could consider separate binary_sensors to report on individual errors
+
+### siren
+- **tone** (required, mapping of strings): a dp to report and control the siren tone. As this is used to turn on and off the siren, it is required. If this does not fit your siren, the underlying implementation will need to be modified.
+The value "off" will be used for turning off the siren, and will be filtered from the list of available tones.
+- **volume** (optional, float in range 0.0-1.0): a dp to control the volume of the siren (probably needs a scale and step applied, since Tuya devices will probably use an integer, or strings with fixed values).
+- **duration** (optional, integer): a dp to control how long the siren will sound for.

+ 2 - 1
custom_components/tuya_local/devices/orion_outdoor_siren.yaml

@@ -15,8 +15,9 @@ primary_entity:
           value: light
         - dps_val: alarm_sound_light
           value: sound+light
+          default: true
         - dps_val: normal
-          value: normal
+          value: "off"
     - id: 5
       name: volume_level
       type: string

+ 28 - 4
custom_components/tuya_local/generic/siren.py

@@ -29,11 +29,18 @@ class TuyaLocalSiren(TuyaLocalEntity, SirenEntity):
         self._init_end(dps_map)
         # All control of features is through the turn_on service, so we need to
         # support that, even if the siren does not support direct control
-        support = SirenEntityFeature.TURN_ON
+        support = 0
         if self._tone_dp:
-            support |= SirenEntityFeature.TONES
+            support |= (
+                SirenEntityFeature.TONES
+                | SirenEntityFeature.TURN_ON
+                | SirenEntityFeature.TURN_OFF
+            )
             self.entity_description = SirenEntityDescription
-            self.entity_description.available_tones = self._tone_dp.values(device)
+            self.entity_description.available_tones = [
+                x for x in self._tone_dp.values(device) if x != "off"
+            ]
+            self._default_tone = self._tone_dp.default()
 
         if self._volume_dp:
             support |= SirenEntityFeature.VOLUME_SET
@@ -41,17 +48,29 @@ class TuyaLocalSiren(TuyaLocalEntity, SirenEntity):
             support |= SirenEntityFeature.DURATION
         self._attr_supported_features = support
 
+    @property
+    def is_on(self):
+        """Return whether the siren is on."""
+        if self._tone_dp:
+            return self._tone_dp.get_value(self._device)
+
     async def async_turn_on(self, **kwargs) -> None:
         tone = kwargs.get("tone", None)
         duration = kwargs.get("duration", None)
         volume = kwargs.get("volume", None)
         set_dps = {}
 
-        if tone is not None and self._tone_dp:
+        if self._tone_dp:
+            if tone is None:
+                tone = self._tone_dp.get_value(self._device)
+                if tone == "off":
+                    tone = self._default_tone
+
             set_dps = {
                 **set_dps,
                 **self._tone_dp.get_values_to_set(self._device, tone),
             }
+
         if duration is not None and self._duration_dp:
             set_dps = {
                 **set_dps,
@@ -74,3 +93,8 @@ class TuyaLocalSiren(TuyaLocalEntity, SirenEntity):
             }
 
         await self._device.async_set_properties(set_dps)
+
+    async def async_turn_off(self) -> None:
+        """Turn off the siren"""
+        if self._tone_dp:
+            await self._tone_dp.async_set_value(self._device, "off")

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

@@ -405,6 +405,17 @@ class TuyaDpsConfig:
         _LOGGER.debug(f"{self.name} values: {val}")
         return list(set(val)) if val else None
 
+    def default(self):
+        """Return the default value for a dp."""
+        if "mapping" not in self._config.keys():
+            _LOGGER.debug(
+                f"No mapping for {self.name}, unable to determine default value"
+            )
+            return None
+        for m in self._config["mapping"]:
+            if m.get("default", False):
+                return m.get("dps_val", None)
+
     def range(self, device, scaled=True):
         """Return the range for this dps if configured."""
         mapping = self._find_map_for_dps(device.get_property(self.id))

+ 20 - 8
tests/devices/test_orion_outdoor_siren.py

@@ -16,6 +16,8 @@ DURATION_DP = "7"
 BATTERY_DP = "15"
 TAMPER_DP = "20"
 
+DEFAULT_TONE = "alarm_sound_light"
+
 
 class TestOrionSiren(MultiBinarySensorTests, BasicSensorTests, TuyaDeviceTestCase):
     __test__ = True
@@ -52,6 +54,7 @@ class TestOrionSiren(MultiBinarySensorTests, BasicSensorTests, TuyaDeviceTestCas
         self.assertEqual(
             self.subject.supported_features,
             SirenEntityFeature.TURN_ON
+            | SirenEntityFeature.TURN_OFF
             | SirenEntityFeature.TONES
             | SirenEntityFeature.DURATION
             | SirenEntityFeature.VOLUME_SET,
@@ -65,7 +68,6 @@ class TestOrionSiren(MultiBinarySensorTests, BasicSensorTests, TuyaDeviceTestCas
                 "sound",
                 "light",
                 "sound+light",
-                "normal",
             ],
         )
 
@@ -90,44 +92,54 @@ class TestOrionSiren(MultiBinarySensorTests, BasicSensorTests, TuyaDeviceTestCas
         ):
             await self.subject.async_turn_on(tone="sound+light")
 
-    async def test_set_to_normal(self):
+    async def test_turn_off(self):
         """Test turning on the siren with various parameters"""
         async with assert_device_properties_set(
             self.subject._device, {TONE_DP: "normal"}
         ):
-            await self.subject.async_turn_on(tone="normal")
+            await self.subject.async_turn_off()
+
+    async def test_turn_on_no_param(self):
+        """Test turning on the siren with no parameters"""
+        async with assert_device_properties_set(
+            self.subject._device,
+            {TONE_DP: DEFAULT_TONE},
+        ):
+            await self.subject.async_turn_on()
 
     async def test_set_volume_low(self):
         """Test turning on the siren with various parameters"""
         async with assert_device_properties_set(
-            self.subject._device, {VOLUME_DP: "low"}
+            self.subject._device, {VOLUME_DP: "low", TONE_DP: DEFAULT_TONE}
         ):
             await self.subject.async_turn_on(volume=0.3)
 
     async def test_set_volume_mid(self):
         """Test turning on the siren with various parameters"""
         async with assert_device_properties_set(
-            self.subject._device, {VOLUME_DP: "middle"}
+            self.subject._device, {VOLUME_DP: "middle", TONE_DP: DEFAULT_TONE}
         ):
             await self.subject.async_turn_on(volume=0.7)
 
     async def test_set_volume_high(self):
         """Test turning on the siren with various parameters"""
         async with assert_device_properties_set(
-            self.subject._device, {VOLUME_DP: "high"}
+            self.subject._device, {VOLUME_DP: "high", TONE_DP: DEFAULT_TONE}
         ):
             await self.subject.async_turn_on(volume=1.0)
 
     async def test_set_volume_mute(self):
         """Test turning on the siren with various parameters"""
         async with assert_device_properties_set(
-            self.subject._device, {VOLUME_DP: "mute"}
+            self.subject._device, {VOLUME_DP: "mute", TONE_DP: DEFAULT_TONE}
         ):
             await self.subject.async_turn_on(volume=0.0)
 
     async def test_set_duration(self):
         """Test turning on the siren with various parameters"""
-        async with assert_device_properties_set(self.subject._device, {DURATION_DP: 5}):
+        async with assert_device_properties_set(
+            self.subject._device, {DURATION_DP: 5, TONE_DP: DEFAULT_TONE}
+        ):
             await self.subject.async_turn_on(duration=5)
 
     async def test_set_multi(self):