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

Add support for SD123 Human Presence Radar.

Issue #146
Jason Rumney 3 лет назад
Родитель
Сommit
454f960b0c

+ 1 - 1
ACKNOWLEDGEMENTS.md

@@ -17,7 +17,7 @@ Further device support has been made with the assistance of users.  Please consi
 - [Xeovar](https://github.com/Xeovar) for assistance in supporting Purline M100 heaters, Garden PAC pool heatpumps and Qoto sprinklers.
 - [paulmfclark](https://github.com/paulmfclark) for assistance in supporting Remora Inverter pool heatpumps
 - [cartman10](https://github.com/cartman10) for assistance with BWT FI 45 pool heater.
- - [superman110](https://github.com/superman110) for assistance in supporting Eanons/purenjoy humidifier.
+ - [superman110](https://github.com/superman110) for assistance in supporting Eanons/purenjoy humidifier and SD123 human presence radar.
  - [woolmonkey](https://github.com/woolmonkey) for assistance in supporting Inkbird ITC306A Thermostat.
  - [hazell20](https://github.com/hazell20) for assistance in supporting Anko fans.
  - [meremortals70](https://github.com/meremortals70) for assistance in supporting Deta fan controllers.

+ 1 - 0
README.md

@@ -158,6 +158,7 @@ Other brands may work with the above configurations
 
 ### Miscellaneous
 - Qoto 03 Smart Water Valve / Sprinkler Controller
+- SD123 HPR01 Human Presence Radar
 
 ---
 

+ 186 - 0
custom_components/tuya_local/devices/sd123_hpr01_presence.yaml

@@ -0,0 +1,186 @@
+name: SD123 Human Presence Radar
+primary_entity:
+  entity: binary_sensor
+  class: occupancy
+  dps:
+    - id: 1
+      type: string
+      name: sensor
+      mapping:
+        - dps_val: none
+          value: false
+        - dps_val: presence
+          value: true
+    - id: 105
+      type: string
+      name: unknown_105
+    - id: 111
+      type: integer
+      name: unknown_111
+secondary_entities:
+  - entity: select
+    name: Safe Range
+    category: config
+    dps:
+      - id: 101
+        type: string
+        name: option
+        mapping:
+          - dps_val: 0_meters
+            value: 0m
+          - dps_val: 1_meters
+            value: 1m
+          - dps_val: 2_meters
+            value: 2m
+          - dps_val: 3_meters
+            value: 3m
+          - dps_val: 4_meters
+            value: 4m
+          - dps_val: 5_meters
+            value: 5m
+          - dps_val: 6_meters
+            value: 6m
+  - entity: select
+    name: Max Range
+    category: config
+    dps:
+      - id: 102
+        type: string
+        name: option
+        mapping:
+          - dps_val: 0_meters
+            value: 0m
+          - dps_val: 1_meters
+            value: 1m
+          - dps_val: 2_meters
+            value: 2m
+          - dps_val: 3_meters
+            value: 3m
+          - dps_val: 4_meters
+            value: 4m
+          - dps_val: 5_meters
+            value: 5m
+          - dps_val: 6_meters
+            value: 6m
+          - dps_val: 7_meters
+            value: 7m
+  - entity: select
+    name: Delay
+    category: config
+    dps:
+      - id: 103
+        type: string
+        name: option
+        mapping:
+          - dps_val: case_0
+            value: 10s
+          - dps_val: case_1
+            value: 30s
+          - dps_val: case_2
+            value: 1m
+          - dps_val: case_3
+            value: 2m
+          - dps_val: case_4
+            value: 5m
+          - dps_val: case_5
+            value: 10m
+          - dps_val: case_6
+            value: 30m
+  - entity: select
+    name: Configuration
+    category: config
+    dps:
+      - id: 104
+        type: string
+        name: option
+        mapping:
+          - dps_val: case_0
+            value: Sleep/Micro motion
+          - dps_val: case_1
+            value: Meeting/Office
+          - dps_val: case_2
+            value: Classroom/Corridor
+          - dps_val: case_3
+            value: Custom
+  - entity: light
+    name: LED
+    category: config
+    dps:
+      - id: 106
+        name: switch
+        type: string
+        mapping:
+          - dps_val: normal
+            value: true
+          - dps_val: slient
+            value: false
+  - entity: number
+    name: Trigger Power
+    category: config
+    dps:
+      - id: 107
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 5000
+        mapping:
+          - step: 100
+  - entity: number
+    name: Maintain Power
+    category: config
+    dps:
+      - id: 108
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 5000
+        mapping:
+          - step: 100
+  - entity: number
+    name: Trigger frames
+    category: config
+    dps:
+      - id: 109
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 20
+  - entity: number
+    name: Interrupt frames
+    category: config
+    dps:
+      - id: 110
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 20
+  - entity: number
+    name: Trigger Points
+    category: config
+    dps:
+      - id: 112
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 10
+  - entity: number
+    name: Maintain Points
+    category: config
+    dps:
+      - id: 113
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 10
+  - entity: switch
+    category: config
+    dps:
+      - id: 114
+        type: boolean
+        name: switch

+ 20 - 0
custom_components/tuya_local/translations/en.json

@@ -67,6 +67,7 @@
                     "light_backlight": "Include backlight as a light entity",
                     "light_display": "Include display as a light entity",
                     "light_flame": "Include flame as a light entity",
+		    "light_led": "Include LED as a light entity",
 		    "light_night_light": "Include night light as a light entity",
                     "light_uv_sterilization": "Include UV sterilization as a light entity",
                     "lock_child_lock": "Include child lock as a lock entity",
@@ -97,12 +98,21 @@
                     "number_timer_3": "Include timer 3 as a number entity",
                     "number_timer_4": "Include timer 4 as a number entity",
                     "number_usb_timer": "Include timer USB as a number entity",
+		    "number_interrupt_frames": "Include interrupt frames as a number entity",
+		    "number_maintain_points": "Include maintain points as a number entity",
+		    "number_maintain_power": "Include maintain power as a number entity",
+		    "number_trigger_frames": "Include trigger frames as a number entity",
+		    "number_trigger_points": "Include trigger points as a number entity",
+		    "number_trigger_power": "Include trigger power as a number entity",
 		    "select_charge_type": "Include charge type as a select entity",
                     "select_configuration": "Include configuration as a select entity",
+		    "select_delay": "Include delay as a select entity",
                     "select_initial_state": "Include initial state as a select entity",
                     "select_installation": "Include installation as a select entity",
+		    "select_max_range": "Include max range as a select entity",
 		    "select_open_window_detection": "Include open window detection as a select entity",
 		    "select_pir_timeout": "Include PIR timeout as a select entity",
+		    "select_safe_range": "Include safe range as a select entity",
                     "select_schedule": "Include schedule as a select entity",
                     "select_timer": "Include timer as a select entity",
                     "select_temperature_sensor": "Include temperature sensor config as a select entity",
@@ -235,6 +245,7 @@
                     "light_backlight": "Include backlight as a light entity",
                     "light_display": "Include display as a light entity",
                     "light_flame": "Include flame as a light entity",
+		    "light_led": "Include LED as a light entity",
 		    "light_night_light": "Include night light as a light entity",
                     "light_uv_sterilization": "Include UV sterilization as a light entity",
                     "lock_child_lock": "Include child lock as a lock entity",
@@ -265,12 +276,21 @@
                     "number_timer_3": "Include timer 3 as a number entity",
                     "number_timer_4": "Include timer 4 as a number entity",
                     "number_usb_timer": "Include timer USB as a number entity",
+		    "number_interrupt_frames": "Include interrupt frames as a number entity",
+		    "number_maintain_points": "Include maintain points as a number entity",
+		    "number_maintain_power": "Include maintain power as a number entity",
+		    "number_trigger_frames": "Include trigger frames as a number entity",
+		    "number_trigger_points": "Include trigger points as a number entity",
+		    "number_trigger_power": "Include trigger power as a number entity",
 		    "select_charge_type": "Include charge type as a select entity",
                     "select_configuration": "Include configuration as a select entity",
+		    "select_delay": "Include delay as a select entity",
                     "select_initial_state": "Include initial state as a select entity",
                     "select_installation": "Include installation as a select entity",
+		    "select_max_range": "Include max range as a select entity",
 		    "select_open_window_detection": "Include open window detection as a select entity",
 		    "select_pir_timeout": "Include PIR timeout as a select entity",
+		    "select_safe_range": "Include safe range as a select entity",
                     "select_schedule": "Include schedule as a select entity",
                     "select_timer": "Include timer as a select entity",
                     "select_temperature_sensor": "Include temperature sensor config as a select entity",

+ 18 - 0
tests/const.py

@@ -1112,3 +1112,21 @@ PARKSIDE_PLGS2012A1_PAYLOAD = {
     "103": False,
     "104": False,
 }
+
+SD123_PRESENCE_PAYLOAD = {
+    "1": "none",
+    "101": "0_meters",
+    "102": "6_meters",
+    "103": "case_0",
+    "104": "case_1",
+    "105": "not_reset",
+    "106": "normal",
+    "107": 1200,
+    "108": 1000,
+    "109": 1,
+    "110": 1,
+    "111": 10,
+    "112": 2,
+    "113": 0,
+    "114": True,
+}

+ 168 - 0
tests/devices/test_sd123_hpr01_presence.py

@@ -0,0 +1,168 @@
+"""Tests for SD123 Human Presence Radar HPR01"""
+from homeassistant.components.binary_sensor import DEVICE_CLASS_OCCUPANCY
+
+from ..const import SD123_PRESENCE_PAYLOAD
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.light import BasicLightTests
+from ..mixins.number import MultiNumberTests
+from ..mixins.select import MultiSelectTests
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
+
+PRESENCE_DPS = "1"
+SAFERANGE_DPS = "101"
+MAXRANGE_DPS = "102"
+DELAY_DPS = "103"
+MODE_DPS = "104"
+UNKNOWN105_DPS = "105"
+LIGHT_DPS = "106"
+TRIGPOW_DPS = "107"
+MAINTPOW_DPS = "108"
+TRIGFRAME_DPS = "109"
+INTFRAME_DPS = "110"
+UNKNOWN111_DPS = "111"
+TRIGPOINT_DPS = "112"
+MAINTPOINT_DPS = "113"
+SWITCH_DPS = "114"
+
+
+class TestSD123HumanPresenceRadar(
+    BasicBinarySensorTests,
+    BasicLightTests,
+    BasicSwitchTests,
+    MultiNumberTests,
+    MultiSelectTests,
+    TuyaDeviceTestCase,
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("sd123_hpr01_presence.yaml", SD123_PRESENCE_PAYLOAD)
+        self.setUpBasicBinarySensor(
+            PRESENCE_DPS,
+            self.entities.get("binary_sensor"),
+            device_class=DEVICE_CLASS_OCCUPANCY,
+            testdata=("presence", "none"),
+        )
+        self.setUpBasicLight(
+            LIGHT_DPS,
+            self.entities.get("light_led"),
+            testdata=("normal", "slient"),
+        )
+        self.setUpBasicSwitch(SWITCH_DPS, self.entities.get("switch"))
+        self.setUpMultiNumber(
+            [
+                {
+                    "name": "number_trigger_power",
+                    "dps": TRIGPOW_DPS,
+                    "max": 5000,
+                    "step": 100,
+                },
+                {
+                    "name": "number_maintain_power",
+                    "dps": MAINTPOW_DPS,
+                    "max": 5000,
+                    "step": 100,
+                },
+                {
+                    "name": "number_trigger_frames",
+                    "dps": TRIGFRAME_DPS,
+                    "max": 20,
+                },
+                {
+                    "name": "number_interrupt_frames",
+                    "dps": INTFRAME_DPS,
+                    "max": 20,
+                },
+                {
+                    "name": "number_trigger_points",
+                    "dps": TRIGPOINT_DPS,
+                    "max": 10,
+                },
+                {
+                    "name": "number_maintain_points",
+                    "dps": MAINTPOINT_DPS,
+                    "max": 10,
+                },
+            ],
+        )
+        self.setUpMultiSelect(
+            [
+                {
+                    "name": "select_safe_range",
+                    "dps": SAFERANGE_DPS,
+                    "options": {
+                        "0_meters": "0m",
+                        "1_meters": "1m",
+                        "2_meters": "2m",
+                        "3_meters": "3m",
+                        "4_meters": "4m",
+                        "5_meters": "5m",
+                        "6_meters": "6m",
+                    },
+                },
+                {
+                    "name": "select_max_range",
+                    "dps": MAXRANGE_DPS,
+                    "options": {
+                        "0_meters": "0m",
+                        "1_meters": "1m",
+                        "2_meters": "2m",
+                        "3_meters": "3m",
+                        "4_meters": "4m",
+                        "5_meters": "5m",
+                        "6_meters": "6m",
+                        "7_meters": "7m",
+                    },
+                },
+                {
+                    "name": "select_delay",
+                    "dps": DELAY_DPS,
+                    "options": {
+                        "case_0": "10s",
+                        "case_1": "30s",
+                        "case_2": "1m",
+                        "case_3": "2m",
+                        "case_4": "5m",
+                        "case_5": "10m",
+                        "case_6": "30m",
+                    },
+                },
+                {
+                    "name": "select_configuration",
+                    "dps": MODE_DPS,
+                    "options": {
+                        "case_0": "Sleep/Micro motion",
+                        "case_1": "Meeting/Office",
+                        "case_2": "Classroom/Corridor",
+                        "case_3": "Custom",
+                    },
+                },
+            ],
+        )
+
+        self.mark_secondary(
+            [
+                "light_led",
+                "number_interrupt_frames",
+                "number_maintain_points",
+                "number_maintain_power",
+                "number_trigger_frames",
+                "number_trigger_points",
+                "number_trigger_power",
+                "select_configuration",
+                "select_delay",
+                "select_max_range",
+                "select_safe_range",
+                "switch",
+            ]
+        )
+
+    def test_basic_bsensor_extra_state_attributes(self):
+        self.dps[UNKNOWN105_DPS] = "unknown_105"
+        self.dps[UNKNOWN111_DPS] = 111
+
+        self.assertDictEqual(
+            self.basicBSensor.extra_state_attributes,
+            {"unknown_105": "unknown_105", "unknown_111": 111},
+        )

+ 11 - 9
tests/mixins/light.py

@@ -8,9 +8,11 @@ from ..helpers import assert_device_properties_set
 
 
 class BasicLightTests:
-    def setUpBasicLight(self, dps, subject):
+    def setUpBasicLight(self, dps, subject, testdata=(True, False)):
         self.basicLight = subject
         self.basicLightDps = dps
+        self.basicLightOn = testdata[0]
+        self.basicLightOff = testdata[1]
 
     def test_basic_light_supported_features(self):
         self.assertEqual(self.basicLight.supported_features, 0)
@@ -32,36 +34,36 @@ class BasicLightTests:
         self.assertIsNone(self.basicLight.effect)
 
     def test_basic_light_is_on(self):
-        self.dps[self.basicLightDps] = True
+        self.dps[self.basicLightDps] = self.basicLightOn
         self.assertTrue(self.basicLight.is_on)
-        self.dps[self.basicLightDps] = False
+        self.dps[self.basicLightDps] = self.basicLightOff
         self.assertFalse(self.basicLight.is_on)
 
     async def test_basic_light_turn_on(self):
         async with assert_device_properties_set(
-            self.basicLight._device, {self.basicLightDps: True}
+            self.basicLight._device, {self.basicLightDps: self.basicLightOn}
         ):
             await self.basicLight.async_turn_on()
 
     async def test_basic_light_turn_off(self):
         async with assert_device_properties_set(
-            self.basicLight._device, {self.basicLightDps: False}
+            self.basicLight._device, {self.basicLightDps: self.basicLightOff}
         ):
             await self.basicLight.async_turn_off()
 
     async def test_basic_light_toggle_turns_on_when_it_was_off(self):
-        self.dps[self.basicLightDps] = False
+        self.dps[self.basicLightDps] = self.basicLightOff
         async with assert_device_properties_set(
             self.basicLight._device,
-            {self.basicLightDps: True},
+            {self.basicLightDps: self.basicLightOn},
         ):
             await self.basicLight.async_toggle()
 
     async def test_basic_light_toggle_turns_off_when_it_was_on(self):
-        self.dps[self.basicLightDps] = True
+        self.dps[self.basicLightDps] = self.basicLightOn
         async with assert_device_properties_set(
             self.basicLight._device,
-            {self.basicLightDps: False},
+            {self.basicLightDps: self.basicLightOff},
         ):
             await self.basicLight.async_toggle()