Explorar el Código

Merge branch 'main' into main

Jason Rumney hace 4 años
padre
commit
442399636e
Se han modificado 42 ficheros con 575 adiciones y 106 borrados
  1. 1 0
      ACKNOWLEDGEMENTS.md
  2. 12 9
      README.md
  3. 15 0
      custom_components/tuya_local/devices/README.md
  4. 2 2
      custom_components/tuya_local/devices/awow_th213_thermostat.yaml
  5. 2 2
      custom_components/tuya_local/devices/beca_bhp6000_thermostat_c.yaml
  6. 2 2
      custom_components/tuya_local/devices/beca_bhp6000_thermostat_f.yaml
  7. 2 2
      custom_components/tuya_local/devices/electriq_cd25pro_dehumidifier.yaml
  8. 2 2
      custom_components/tuya_local/devices/goldair_dehumidifier.yaml
  9. 2 2
      custom_components/tuya_local/devices/goldair_geco_heater.yaml
  10. 2 2
      custom_components/tuya_local/devices/goldair_gpcv_heater.yaml
  11. 2 2
      custom_components/tuya_local/devices/goldair_gpph_heater.yaml
  12. 2 2
      custom_components/tuya_local/devices/kogan_kahtp_heater.yaml
  13. 2 2
      custom_components/tuya_local/devices/kogan_kawfhtp_heater.yaml
  14. 2 2
      custom_components/tuya_local/devices/lexy_f501_fan.yaml
  15. 72 0
      custom_components/tuya_local/devices/renpho_rp_ap001s.yaml
  16. 1 1
      custom_components/tuya_local/manifest.json
  17. 1 1
      hacs.json
  18. 37 0
      tests/const.py
  19. 131 0
      tests/devices/test_arlec_fan.py
  20. 3 3
      tests/devices/test_awow_th213_thermostat.py
  21. 7 7
      tests/devices/test_eanons_humidifier.py
  22. 1 1
      tests/devices/test_electriq_12wminv_heatpump.py
  23. 1 1
      tests/devices/test_electriq_cd20_dehumidifier.py
  24. 1 1
      tests/devices/test_electriq_cd25_dehumidifier.py
  25. 1 1
      tests/devices/test_fersk_vind_2_climate.py
  26. 1 1
      tests/devices/test_gardenpac_heatpump.py
  27. 3 3
      tests/devices/test_goldair_dehumidifier.py
  28. 4 3
      tests/devices/test_goldair_geco_heater.py
  29. 4 3
      tests/devices/test_goldair_gpcv_heater.py
  30. 2 2
      tests/devices/test_goldair_gpph_heater.py
  31. 1 1
      tests/devices/test_hellnar_heatpump.py
  32. 2 2
      tests/devices/test_inkbird_itc306a_thermostat.py
  33. 3 3
      tests/devices/test_kogan_dehumidifier.py
  34. 4 3
      tests/devices/test_kogan_kahtp_heater.py
  35. 1 3
      tests/devices/test_kogan_kawfhtp_heater.py
  36. 1 1
      tests/devices/test_madimack_heatpump.py
  37. 237 0
      tests/devices/test_renpho_rp_ap001s.py
  38. 1 1
      tests/devices/test_saswell_t29utk_thermostat.py
  39. 1 15
      tests/devices/test_smartplugv1.py
  40. 1 15
      tests/devices/test_smartplugv2.py
  41. 2 2
      tests/devices/test_tadiran_wind_heatpump.py
  42. 1 1
      tests/devices/test_wetair_wch750_heater.py

+ 1 - 0
ACKNOWLEDGEMENTS.md

@@ -38,3 +38,4 @@ Further device support has been made with the assistance of users.  Please consi
  - [Vikedlol](https://github.com/Vikedlol) for assistance in supporting Wetair WCH-750 heaters.
  - [wwalczyszyn](https://github.com/wwalczyszyn) for contributing support for Fersk Vind 2 heatpumps.
  - [xbmcnut](https://github.com/xbmcnut) for assistance in supporting Kogan Smart Kettles and the new type of Kogan heater.
+ - [ThomasADavis](https://github.com/ThomasADavis) for contributing support for Renpho RP-AP001S air purifiers.

+ 12 - 9
README.md

@@ -44,12 +44,24 @@ Note that devices sometimes get firmware upgrades, or incompatible versions are
   or if any of the "unknown" values that are returned as attributes can
   be figured out.
 
+### Thermostats
+- Inkbird ITC306A thermostat smartplug (not fully functional)
+- Beca BHP-6000 Room Heat Pump control Thermostat
+- Awow/Mi-heat TH213 Thermostat
+- Siswell T29UTW Thermostat
+
+### Kettles
+- Kogan Glass 1.7L Smart Kettle
+
 ### Fans
 - Goldair GCPF315 fan
 - Anko HEGSM40 fan
 - Lexy F501 fan
 - Deta fan controller
 
+### Air Purifiers
+- Renpho RP-AP001S air purifier
+
 ### Dehumidifiers
 - Goldair GPDH420 dehumidifier
 - ElectriQ CD20PRO-LE-V2 dehumidifier
@@ -60,15 +72,6 @@ Note that devices sometimes get firmware upgrades, or incompatible versions are
 ### Humidifiers
 - Eanons QT-JS2014 Purifying humidifer
 
-### Thermostats
-- Inkbird ITC306A thermostat smartplug (not fully functional)
-- Beca BHP-6000 Room Heat Pump control Thermostat
-- Awow/Mi-heat TH213 Thermostat
-- Siswell T29UTW Thermostat
-
-### Kettles
-- Kogan Glass 1.7L Smart Kettle
-
 ### SmartPlugs
 - Kogan Single Smartplug with Energy Monitoring
 - Kogan Single Smartplug with Energy Monitoring and USB charging

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

@@ -27,6 +27,21 @@ was known by.  It is used in the migration process to migrate old
 configs to the latest config which uses the config filename as the identifier
 for the device.  New devices should not define this.
 
+### `products`
+
+// Optional, for future use. //
+
+A list of products that this config applies to.  Each product in the list must
+have an `id` specified, which corresponds to the productId or productKey
+(depending on where you are getting it from) in Tuya info.  This is available
+from the Tuya developer web portal listing for your device, or when using
+UDP discovery (via tinytuya).  In future it is intended that UDP discovery
+will be used to more precisely match devices to configs, so it is recommended
+to report these if you can find them when requesting a new device.  Each
+listing can also have an optional `name`, which is intended to override the
+top level `name` when full support for this field is added.
+Probably other info will be added in future to provide better reporting of
+device manufacturer and model etc.
 
 ### `primary_entity`
 

+ 2 - 2
custom_components/tuya_local/devices/awow_th213_thermostat.yaml

@@ -98,6 +98,6 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"

+ 2 - 2
custom_components/tuya_local/devices/beca_bhp6000_thermostat_c.yaml

@@ -62,9 +62,9 @@ secondary_entities:
         type: boolean
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"
   - entity: light
     name: Display
     dps:

+ 2 - 2
custom_components/tuya_local/devices/beca_bhp6000_thermostat_f.yaml

@@ -63,9 +63,9 @@ secondary_entities:
         type: boolean
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"
   - entity: light
     name: Display
     dps:

+ 2 - 2
custom_components/tuya_local/devices/electriq_cd25pro_dehumidifier.yaml

@@ -85,6 +85,6 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"

+ 2 - 2
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -244,9 +244,9 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"
   - entity: switch
     name: Air Clean
     icon: "mdi:air-purifier"

+ 2 - 2
custom_components/tuya_local/devices/goldair_geco_heater.yaml

@@ -42,7 +42,7 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"
 

+ 2 - 2
custom_components/tuya_local/devices/goldair_gpcv_heater.yaml

@@ -50,6 +50,6 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"

+ 2 - 2
custom_components/tuya_local/devices/goldair_gpph_heater.yaml

@@ -138,6 +138,6 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"

+ 2 - 2
custom_components/tuya_local/devices/kogan_kahtp_heater.yaml

@@ -43,6 +43,6 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"

+ 2 - 2
custom_components/tuya_local/devices/kogan_kawfhtp_heater.yaml

@@ -42,6 +42,6 @@ secondary_entities:
         name: lock
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"

+ 2 - 2
custom_components/tuya_local/devices/lexy_f501_fan.yaml

@@ -66,9 +66,9 @@ secondary_entities:
         type: boolean
         mapping:
           - dps_val: true
-            icon: "mdi:account-lock"
+            icon: "mdi:hand-back-right-off"
           - dps_val: false
-            icon: "mdi:account"
+            icon: "mdi:hand-back-right"
   - entity: switch
     name: Sound
     dps:

+ 72 - 0
custom_components/tuya_local/devices/renpho_rp_ap001s.yaml

@@ -0,0 +1,72 @@
+name: Renpho RP-AP001S
+products:
+  - id: oaleouzsq0sk4quk
+primary_entity:
+  entity: fan
+  icon: "mdi:air-purifier"
+  dps:
+    - id: 1
+      name: switch
+      type: boolean
+    - id: 4
+      name: preset_mode
+      type: string
+      mapping:
+        - dps_val: "low"
+          value: low
+        - dps_val: "mid"
+          value: mid
+        - dps_val: "high"
+          value: high
+        - dps_val: "auto"
+          value: auto
+    - id: 19
+      name: timer
+      type: string
+    - id: 22
+      name: air_quality
+      type: string
+    - id: 102
+      name: prefilter_life
+      type: integer
+    - id: 103
+      name: charcoal_filter_life
+      type: integer
+    - id: 104
+      name: activated_charcoal_filter_life
+      type: integer
+    - id: 105
+      name: hepa_filter_life
+      type: integer
+secondary_entities:
+  - entity: lock
+    name: Child Lock
+    dps:
+      - id: 7
+        name: lock
+        type: boolean
+        mapping:
+          - dps_val: true
+            icon: "mdi:hand-back-right"
+          - dps_val: false
+            icon: "mdi:hand-back-right-off"
+  - entity: light
+    name: AQI mode
+    class: switch
+    dps:
+      - id: 8
+        name: switch
+        type: boolean
+        mapping:
+          - dps_val: true
+            icon: "mdi:led-on"
+          - dps_val: false
+            icon: "mdi:led-off"
+  - entity: switch
+    name: Sleep Mode
+    class: switch
+    icon: "mdi:power-sleep"
+    dps:
+      - id: 101
+        name: switch
+        type: boolean

+ 1 - 1
custom_components/tuya_local/manifest.json

@@ -2,7 +2,7 @@
     "domain": "tuya_local",
     "iot_class": "local_polling",
     "name": "Tuya Local",
-    "version": "0.11.1",
+    "version": "0.11.2",
     "documentation": "https://github.com/make-all/tuya-local",
     "issue_tracker": "https://github.com/make-all/tuya-local/issues",
     "dependencies": [],

+ 1 - 1
hacs.json

@@ -2,6 +2,6 @@
   "name": "Tuya Local",
   "render_readme": true,
   "domains": ["climate", "fan", "humidifier", "light", "lock", "switch"],
-  "homeassistant": "2021.5.0",
+  "homeassistant": "2021.10.0",
   "iot_class": "Local Polling"
 }

+ 37 - 0
tests/const.py

@@ -400,3 +400,40 @@ KOGAN_GLASS_1_7L_KETTLE_PAYLOAD = {
     "5": 99,
     "102": "90",
 }
+
+RENPHO_PURIFIER_PAYLOAD = {
+    "1": True,
+    "4": "low",
+    "7": False,
+    "8": False,
+    "19": "0",
+    "22": "0",
+    "101": False,
+    "102": 0,
+    "103": 0,
+    "104": 0,
+    "105": 0,
+}
+
+ARLEC_FAN_PAYLOAD = {
+    "1": True,
+    "3": 1,
+    "4": "forward",
+    "102": "normal",
+    "103": 0,
+}
+
+CARSON_CB_PAYOAD = {
+    "1": True,
+    "2": 20,
+    "3": 23,
+    "4": "COOL",
+    "5": 1,
+    "19": "C",
+    "102": False,
+    "103": 0,
+    "104": False,
+    "105": 0,
+    "106": 0,
+    "110": 0,
+}

+ 131 - 0
tests/devices/test_arlec_fan.py

@@ -0,0 +1,131 @@
+from homeassistant.components.fan import (
+    DIRECTION_FORWARD,
+    DIRECTION_REVERSE,
+    SUPPORT_DIRECTION,
+    SUPPORT_PRESET_MODE,
+    SUPPORT_SET_SPEED,
+)
+
+from homeassistant.const import STATE_UNAVAILABLE
+
+from ..const import ARLEC_FAN_PAYLOAD
+from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
+
+SWITCH_DPS = "1"
+SPEED_DPS = "3"
+DIRECTION_DPS = "4"
+PRESET_DPS = "102"
+TIMER_DPS = "103"
+
+
+class TestArlecFan(TuyaDeviceTestCase):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("arlec_fan.yaml", ARLEC_FAN_PAYLOAD)
+        self.subject = self.entities["fan"]
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            SUPPORT_DIRECTION | SUPPORT_PRESET_MODE | SUPPORT_SET_SPEED,
+        )
+
+    def test_is_on(self):
+        self.dps[SWITCH_DPS] = True
+        self.assertTrue(self.subject.is_on)
+
+        self.dps[SWITCH_DPS] = False
+        self.assertFalse(self.subject.is_on)
+
+        self.dps[SWITCH_DPS] = None
+        self.assertEqual(self.subject.is_on, STATE_UNAVAILABLE)
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: True}
+        ):
+            await self.subject.async_turn_on()
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: False}
+        ):
+            await self.subject.async_turn_off()
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "normal"
+        self.assertEqual(self.subject.preset_mode, "normal")
+
+        self.dps[PRESET_DPS] = "breeze"
+        self.assertEqual(self.subject.preset_mode, "breeze")
+
+        self.dps[PRESET_DPS] = "sleep"
+        self.assertEqual(self.subject.preset_mode, "sleep")
+
+        self.dps[PRESET_DPS] = None
+        self.assertIs(self.subject.preset_mode, None)
+
+    def test_preset_modes(self):
+        self.assertCountEqual(self.subject.preset_modes, ["normal", "breeze", "sleep"])
+
+    async def test_set_preset_mode_to_normal(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "normal"},
+        ):
+            await self.subject.async_set_preset_mode("normal")
+
+    async def test_set_preset_mode_to_breeze(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "breeze"},
+        ):
+            await self.subject.async_set_preset_mode("breeze")
+
+    async def test_set_preset_mode_to_sleep(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "sleep"},
+        ):
+            await self.subject.async_set_preset_mode("sleep")
+
+    def test_direction(self):
+        self.dps[DIRECTION_DPS] = "forward"
+        self.assertEqual(self.subject.current_direction, DIRECTION_FORWARD)
+        self.dps[DIRECTION_DPS] = "reverse"
+        self.assertEqual(self.subject.current_direction, DIRECTION_REVERSE)
+
+    async def test_set_direction_forward(self):
+        async with assert_device_properties_set(
+            self.subject._device, {DIRECTION_DPS: "forward"}
+        ):
+            await self.subject.async_set_direction(DIRECTION_FORWARD)
+
+    async def test_set_direction_reverse(self):
+        async with assert_device_properties_set(
+            self.subject._device, {DIRECTION_DPS: "reverse"}
+        ):
+            await self.subject.async_set_direction(DIRECTION_REVERSE)
+
+    def test_speed(self):
+        self.dps[SPEED_DPS] = "3"
+        self.assertEqual(self.subject.percentage, 50)
+
+    def test_speed_step(self):
+        self.assertAlmostEqual(self.subject.percentage_step, 16.67, 2)
+        self.assertEqual(self.subject.speed_count, 6)
+
+    async def test_set_speed(self):
+        async with assert_device_properties_set(self.subject._device, {SPEED_DPS: 2}):
+            await self.subject.async_set_percentage(33)
+
+    async def test_set_speed_in_normal_mode_snaps(self):
+        self.dps[PRESET_DPS] = "normal"
+        async with assert_device_properties_set(self.subject._device, {SPEED_DPS: 5}):
+            await self.subject.async_set_percentage(80)
+
+    def test_device_state_attributes(self):
+        self.dps[TIMER_DPS] = "5"
+        self.assertEqual(self.subject.device_state_attributes, {"timer": 5})

+ 3 - 3
tests/devices/test_awow_th213_thermostat.py

@@ -52,10 +52,10 @@ class TestAwowTH213Thermostat(TuyaDeviceTestCase):
         self.assertEqual(self.subject.icon, "mdi:thermometer-off")
 
         self.dps[LOCK_DPS] = True
-        self.assertEqual(self.lock.icon, "mdi:account-lock")
+        self.assertEqual(self.lock.icon, "mdi:hand-back-right-off")
 
         self.dps[LOCK_DPS] = False
-        self.assertEqual(self.lock.icon, "mdi:account")
+        self.assertEqual(self.lock.icon, "mdi:hand-back-right")
 
     def test_temperature_unit_returns_device_temperature_unit(self):
         self.assertEqual(
@@ -225,7 +225,7 @@ class TestAwowTH213Thermostat(TuyaDeviceTestCase):
         self.dps[UNKNOWN108_DPS] = False
         self.dps[UNKNOWN110_DPS] = 110
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "error": 8,

+ 7 - 7
tests/devices/test_eanons_humidifier.py

@@ -167,11 +167,11 @@ class TestEanonsHumidifier(TuyaDeviceTestCase):
     def test_preset_modes(self):
         self.assertCountEqual(
             self.climate.preset_modes,
-            {MODE_NORMAL, MODE_SLEEP, MODE_AUTO},
+            [MODE_NORMAL, MODE_SLEEP, MODE_AUTO],
         )
         self.assertCountEqual(
             self.subject.available_modes,
-            {MODE_NORMAL, MODE_SLEEP, MODE_AUTO},
+            [MODE_NORMAL, MODE_SLEEP, MODE_AUTO],
         )
 
     async def test_set_climate_preset_to_auto(self):
@@ -238,7 +238,7 @@ class TestEanonsHumidifier(TuyaDeviceTestCase):
         self.dps[ERROR_DPS] = 0
         self.dps[TIMERHR_DPS] = "cancel"
         self.dps[TIMER_DPS] = 0
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.climate.device_state_attributes,
             {
                 "error": "OK",
@@ -250,10 +250,10 @@ class TestEanonsHumidifier(TuyaDeviceTestCase):
         self.dps[ERROR_DPS] = 1
         self.dps[TIMERHR_DPS] = "1"
         self.dps[TIMER_DPS] = 60
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.climate.device_state_attributes,
             {
-                "error": 1,
+                "error": "Water Level Low",
                 "timer_hr": "1",
                 "timer_min": 60,
             },
@@ -266,7 +266,7 @@ class TestEanonsHumidifier(TuyaDeviceTestCase):
         self.dps[CURRENTHUMID_DPS] = 50
         self.dps[FANMODE_DPS] = "middle"
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "error": "OK",
@@ -308,7 +308,7 @@ class TestEanonsHumidifier(TuyaDeviceTestCase):
     def test_climate_fan_modes(self):
         self.assertCountEqual(
             self.climate.fan_modes,
-            {FAN_LOW, FAN_MEDIUM, FAN_HIGH},
+            [FAN_LOW, FAN_MEDIUM, FAN_HIGH],
         )
 
     async def test_fan_set_speed(self):

+ 1 - 1
tests/devices/test_electriq_12wminv_heatpump.py

@@ -291,7 +291,7 @@ class TestElectriq12WMINVHeatpump(TuyaDeviceTestCase):
         self.dps[UNKNOWN108_DPS] = 108
         self.dps[UNKNOWN109_DPS] = 109
         self.dps[UNKNOWN110_DPS] = 110
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "unknown_8": True,

+ 1 - 1
tests/devices/test_electriq_cd20_dehumidifier.py

@@ -272,7 +272,7 @@ class TestElectriqCD20ProDehumidifier(TuyaDeviceTestCase):
         self.dps[CURRENTHUMID_DPS] = 50
         self.dps[CURRENTTEMP_DPS] = 21
         self.dps[ANION_DPS] = True
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {"current_humidity": 50, "current_temperature": 21, "anion": True},
         )

+ 1 - 1
tests/devices/test_electriq_cd25_dehumidifier.py

@@ -295,7 +295,7 @@ class TestElectriqCD25ProDehumidifier(TuyaDeviceTestCase):
     def test_state_attributes(self):
         self.dps[CURRENTHUMID_DPS] = 50
         self.dps[CURRENTTEMP_DPS] = 21
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {"current_humidity": 50, "current_temperature": 21},
         )

+ 1 - 1
tests/devices/test_fersk_vind_2_climate.py

@@ -253,7 +253,7 @@ class TestFerskVind2Climate(TuyaDeviceTestCase):
         self.dps[UNKNOWN106_DPS] = 106
         self.dps[UNKNOWN109_DPS] = True
         self.dps[UNKNOWN110_DPS] = 110
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "unknown_101": True,

+ 1 - 1
tests/devices/test_gardenpac_heatpump.py

@@ -171,7 +171,7 @@ class TestGardenPACPoolHeatpump(TuyaDeviceTestCase):
         self.dps[UNKNOWN108_DPS] = 2
         self.dps[UNKNOWN115_DPS] = 3
         self.dps[UNKNOWN116_DPS] = 4
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "power_level": 50,

+ 3 - 3
tests/devices/test_goldair_dehumidifier.py

@@ -528,10 +528,10 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
         self.dps[AIRCLEAN_DPS] = False
         self.dps[UNKNOWN12_DPS] = "something"
         self.dps[UNKNOWN101_DPS] = False
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.climate.device_state_attributes,
             {
-                "error": STATE_UNAVAILABLE,
+                "error": None,
                 "defrosting": False,
                 "air_clean_on": False,
                 "unknown_12": "something",
@@ -544,7 +544,7 @@ class TestGoldairDehumidifier(TuyaDeviceTestCase):
         self.dps[AIRCLEAN_DPS] = True
         self.dps[UNKNOWN12_DPS] = "something else"
         self.dps[UNKNOWN101_DPS] = True
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.climate.device_state_attributes,
             {
                 "error": ERROR_TANK,

+ 4 - 3
tests/devices/test_goldair_geco_heater.py

@@ -125,12 +125,13 @@ class TestGoldairGECOHeater(TuyaDeviceTestCase):
         # they are discovered
         self.dps[ERROR_DPS] = "something"
         self.dps[TIMER_DPS] = 10
-        self.assertCountEqual(
-            self.subject.device_state_attributes, {"error": "something", "timer": 10}
+        self.assertDictEqual(
+            self.subject.device_state_attributes,
+            {"error": "something", "timer": 10},
         )
         self.dps[ERROR_DPS] = "0"
         self.dps[TIMER_DPS] = 0
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes, {"error": "OK", "timer": 0}
         )
 

+ 4 - 3
tests/devices/test_goldair_gpcv_heater.py

@@ -166,12 +166,13 @@ class TestGoldairGPCVHeater(TuyaDeviceTestCase):
         # they are discovered
         self.dps[ERROR_DPS] = "something"
         self.dps[TIMER_DPS] = 10
-        self.assertCountEqual(
-            self.subject.device_state_attributes, {"error": "something", "timer": 10}
+        self.assertDictEqual(
+            self.subject.device_state_attributes,
+            {"error": "something", "timer": 10},
         )
         self.dps[ERROR_DPS] = "0"
         self.dps[TIMER_DPS] = 0
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes, {"error": "OK", "timer": 0}
         )
 

+ 2 - 2
tests/devices/test_goldair_gpph_heater.py

@@ -319,13 +319,13 @@ class TestGoldairHeater(TuyaDeviceTestCase):
         self.dps[TIMERACT_DPS] = True
         self.dps[POWERLEVEL_DPS] = 4
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "error": "something",
                 "timer": 5,
                 "timer_mode": True,
-                "power_level": 4,
+                "power_level": "4",
             },
         )
 

+ 1 - 1
tests/devices/test_hellnar_heatpump.py

@@ -179,7 +179,7 @@ class TestHellnarHeatpump(TuyaDeviceTestCase):
         self.dps["133"] = "unknown133"
         self.dps["134"] = "unknown134"
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "maybe_fan_mode": "fan?",

+ 2 - 2
tests/devices/test_inkbird_itc306a_thermostat.py

@@ -89,7 +89,7 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
     def test_preset_modes(self):
         self.assertCountEqual(
             self.subject.preset_modes,
-            {"On", "Pause", "Off"},
+            ["On", "Pause", "Off"],
         )
 
     async def test_set_preset_to_on(self):
@@ -196,7 +196,7 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
         self.dps[UNKNOWN119_DPS] = True
         self.dps[UNKNOWN120_DPS] = False
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "error": 1,

+ 3 - 3
tests/devices/test_kogan_dehumidifier.py

@@ -99,12 +99,12 @@ class TestKoganDehumidifier(TuyaDeviceTestCase):
     def test_modes(self):
         self.assertCountEqual(
             self.subject.available_modes,
-            {
+            [
                 "Low",
                 "Medium",
                 "High",
                 "Dry Clothes",
-            },
+            ],
         )
 
     def test_mode(self):
@@ -229,7 +229,7 @@ class TestKoganDehumidifier(TuyaDeviceTestCase):
         self.dps[ERROR_DPS] = 1
         self.dps[TIMER_DPS] = 3
         self.dps[COUNTDOWN_DPS] = 160
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "current_humidity": 55,

+ 4 - 3
tests/devices/test_kogan_kahtp_heater.py

@@ -162,9 +162,10 @@ class TestGoldairKoganKAHTPHeater(TuyaDeviceTestCase):
 
     def test_state_attributes(self):
         self.dps[TIMER_DPS] = 10
-        self.assertCountEqual(self.subject.device_state_attributes, {"timer": 10})
-        self.dps[TIMER_DPS] = 0
-        self.assertCountEqual(self.subject.device_state_attributes, {"timer": 0})
+        self.assertDictEqual(
+            self.subject.device_state_attributes,
+            {"timer": 10},
+        )
 
     def test_lock_state(self):
         self.dps[LOCK_DPS] = True

+ 1 - 3
tests/devices/test_kogan_kawfhtp_heater.py

@@ -162,9 +162,7 @@ class TestGoldairKoganKAHTPHeater(TuyaDeviceTestCase):
 
     def test_state_attributes(self):
         self.dps[TIMER_DPS] = 10
-        self.assertCountEqual(self.subject.device_state_attributes, {"timer": 10})
-        self.dps[TIMER_DPS] = 0
-        self.assertCountEqual(self.subject.device_state_attributes, {"timer": 0})
+        self.assertDictEqual(self.subject.device_state_attributes, {"timer": 10})
 
     def test_lock_state(self):
         self.dps[LOCK_DPS] = True

+ 1 - 1
tests/devices/test_madimack_heatpump.py

@@ -201,7 +201,7 @@ class TestMadimackPoolHeatpump(TuyaDeviceTestCase):
         self.dps[UNKNOWN136_DPS] = False
         self.dps[UNKNOWN139_DPS] = True
         self.dps[UNKNOWN140_DPS] = "test"
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "power_level": 50,

+ 237 - 0
tests/devices/test_renpho_rp_ap001s.py

@@ -0,0 +1,237 @@
+from homeassistant.components.fan import SUPPORT_PRESET_MODE
+from homeassistant.components.light import COLOR_MODE_ONOFF
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.const import STATE_UNAVAILABLE
+
+from ..const import RENPHO_PURIFIER_PAYLOAD
+from ..helpers import assert_device_properties_set
+from .base_device_tests import TuyaDeviceTestCase
+
+SWITCH_DPS = "1"
+PRESET_DPS = "4"
+LOCK_DPS = "7"
+LIGHT_DPS = "8"
+TIMER_DPS = "19"
+QUALITY_DPS = "22"
+SLEEP_DPS = "101"
+PREFILTER_DPS = "102"
+CHARCOAL_DPS = "103"
+ACTIVATED_DPS = "104"
+HEPA_DPS = "105"
+
+
+class TestRenphoPurifier(TuyaDeviceTestCase):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("renpho_rp_ap001s.yaml", RENPHO_PURIFIER_PAYLOAD)
+        self.subject = self.entities["fan"]
+        self.light = self.entities["light"]
+        self.lock = self.entities["lock"]
+        self.switch = self.entities["switch"]
+
+    def test_supported_features(self):
+        self.assertEqual(self.subject.supported_features, SUPPORT_PRESET_MODE)
+
+    def test_is_on(self):
+        self.dps[SWITCH_DPS] = True
+        self.assertTrue(self.subject.is_on)
+        self.dps[SWITCH_DPS] = False
+        self.assertFalse(self.subject.is_on)
+        self.dps[SWITCH_DPS] = None
+        self.assertEqual(self.subject.is_on, STATE_UNAVAILABLE)
+
+    async def test_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {SWITCH_DPS: True},
+        ):
+            await self.subject.async_turn_on()
+
+    async def test_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {SWITCH_DPS: False},
+        ):
+            await self.subject.async_turn_off()
+
+    def test_preset_modes(self):
+        self.assertCountEqual(
+            self.subject.preset_modes,
+            ["low", "mid", "high", "auto"],
+        )
+
+    def test_preset_mode(self):
+        self.dps[PRESET_DPS] = "low"
+        self.assertEqual(self.subject.preset_mode, "low")
+        self.dps[PRESET_DPS] = "mid"
+        self.assertEqual(self.subject.preset_mode, "mid")
+        self.dps[PRESET_DPS] = "high"
+        self.assertEqual(self.subject.preset_mode, "high")
+        self.dps[PRESET_DPS] = "auto"
+        self.assertEqual(self.subject.preset_mode, "auto")
+
+    async def test_set_preset_mode_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "low"},
+        ):
+            await self.subject.async_set_preset_mode("low")
+
+    async def test_set_preset_mode_to_mid(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "mid"},
+        ):
+            await self.subject.async_set_preset_mode("mid")
+
+    async def test_set_preset_mode_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "high"},
+        ):
+            await self.subject.async_set_preset_mode("high")
+
+    async def test_set_preset_mode_to_auto(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {PRESET_DPS: "auto"},
+        ):
+            await self.subject.async_set_preset_mode("auto")
+
+    def test_device_state_attributes(self):
+        self.dps[TIMER_DPS] = "19"
+        self.dps[QUALITY_DPS] = "22"
+        self.dps[PREFILTER_DPS] = 102
+        self.dps[CHARCOAL_DPS] = 103
+        self.dps[ACTIVATED_DPS] = 104
+        self.dps[HEPA_DPS] = 105
+
+        self.assertDictEqual(
+            self.subject.device_state_attributes,
+            {
+                "timer": "19",
+                "air_quality": "22",
+                "prefilter_life": 102,
+                "charcoal_filter_life": 103,
+                "activated_charcoal_filter_life": 104,
+                "hepa_filter_life": 105,
+            },
+        )
+
+    def test_lock_state(self):
+        self.dps[LOCK_DPS] = True
+        self.assertEqual(self.lock.state, STATE_LOCKED)
+
+        self.dps[LOCK_DPS] = False
+        self.assertEqual(self.lock.state, STATE_UNLOCKED)
+
+        self.dps[LOCK_DPS] = None
+        self.assertEqual(self.lock.state, STATE_UNAVAILABLE)
+
+    def test_lock_is_locked(self):
+        self.dps[LOCK_DPS] = True
+        self.assertTrue(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = False
+        self.assertFalse(self.lock.is_locked)
+
+        self.dps[LOCK_DPS] = None
+        self.assertFalse(self.lock.is_locked)
+
+    async def test_lock_locks(self):
+        async with assert_device_properties_set(
+            self.lock._device,
+            {LOCK_DPS: True},
+        ):
+            await self.lock.async_lock()
+
+    async def test_lock_unlocks(self):
+        async with assert_device_properties_set(
+            self.lock._device,
+            {LOCK_DPS: False},
+        ):
+            await self.lock.async_unlock()
+
+    def test_light_supported_color_modes(self):
+        self.assertCountEqual(
+            self.light.supported_color_modes,
+            [COLOR_MODE_ONOFF],
+        )
+
+    def test_light_color_mode(self):
+        self.assertEqual(self.light.color_mode, COLOR_MODE_ONOFF)
+
+    def test_light_has_no_brightness(self):
+        self.assertIsNone(self.light.brightness)
+
+    def test_light_icon(self):
+        self.dps[LIGHT_DPS] = True
+        self.assertEqual(self.light.icon, "mdi:led-on")
+
+        self.dps[LIGHT_DPS] = False
+        self.assertEqual(self.light.icon, "mdi:led-off")
+
+    def test_light_is_on(self):
+        self.dps[LIGHT_DPS] = True
+        self.assertEqual(self.light.is_on, True)
+
+        self.dps[LIGHT_DPS] = False
+        self.assertEqual(self.light.is_on, False)
+
+    async def test_light_turn_on(self):
+        async with assert_device_properties_set(self.light._device, {LIGHT_DPS: True}):
+            await self.light.async_turn_on()
+
+    async def test_light_turn_off(self):
+        async with assert_device_properties_set(self.light._device, {LIGHT_DPS: False}):
+            await self.light.async_turn_off()
+
+    async def test_toggle_turns_the_light_on_when_it_was_off(self):
+        self.dps[LIGHT_DPS] = False
+
+        async with assert_device_properties_set(self.light._device, {LIGHT_DPS: True}):
+            await self.light.async_toggle()
+
+    async def test_toggle_turns_the_light_off_when_it_was_on(self):
+        self.dps[LIGHT_DPS] = True
+
+        async with assert_device_properties_set(self.light._device, {LIGHT_DPS: False}):
+            await self.light.async_toggle()
+
+    def test_switch_icon(self):
+        self.assertEqual(self.switch.icon, "mdi:power-sleep")
+
+    def test_switch_is_on(self):
+        self.dps[SLEEP_DPS] = True
+        self.assertEqual(self.switch.is_on, True)
+
+        self.dps[SLEEP_DPS] = False
+        self.assertEqual(self.switch.is_on, False)
+
+    def test_switch_state_attributes(self):
+        self.assertEqual(self.switch.device_state_attributes, {})
+
+    async def test_switch_turn_on(self):
+        async with assert_device_properties_set(self.switch._device, {SLEEP_DPS: True}):
+            await self.switch.async_turn_on()
+
+    async def test_switch_turn_off(self):
+        async with assert_device_properties_set(
+            self.switch._device, {SLEEP_DPS: False}
+        ):
+            await self.switch.async_turn_off()
+
+    async def test_toggle_turns_the_switch_on_when_it_was_off(self):
+        self.dps[SLEEP_DPS] = False
+
+        async with assert_device_properties_set(self.switch._device, {SLEEP_DPS: True}):
+            await self.switch.async_toggle()
+
+    async def test_toggle_turns_the_switch_off_when_it_was_on(self):
+        self.dps[SLEEP_DPS] = True
+
+        async with assert_device_properties_set(
+            self.switch._device, {SLEEP_DPS: False}
+        ):
+            await self.switch.async_toggle()

+ 1 - 1
tests/devices/test_saswell_t29utk_thermostat.py

@@ -240,7 +240,7 @@ class TestSaswellT29UTKThermostat(TuyaDeviceTestCase):
         self.dps[TEMPF_DPS] = 116
         self.dps[CURTEMPF_DPS] = 117
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "set_temperature": 0.2,

+ 1 - 15
tests/devices/test_smartplugv1.py

@@ -71,7 +71,7 @@ class TestKoganSwitch(TuyaDeviceTestCase):
         self.dps[VOLTAGE_DPS] = 2350
         self.dps[CURRENT_DPS] = 1234
         self.dps[POWER_DPS] = 5678
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "timer": 1,
@@ -80,17 +80,3 @@ class TestKoganSwitch(TuyaDeviceTestCase):
                 "current_power_w": 567.8,
             },
         )
-
-        self.dps[TIMER_DPS] = 0
-        self.dps[CURRENT_DPS] = None
-        self.dps[VOLTAGE_DPS] = None
-        self.dps[POWER_DPS] = None
-        self.assertCountEqual(
-            self.subject.device_state_attributes,
-            {
-                "timer": 0,
-                "current_a": None,
-                "voltage_v": None,
-                "current_power_w": None,
-            },
-        )

+ 1 - 15
tests/devices/test_smartplugv2.py

@@ -71,7 +71,7 @@ class TestSwitchV2(TuyaDeviceTestCase):
         self.dps[VOLTAGE_DPS] = 2350
         self.dps[CURRENT_DPS] = 1234
         self.dps[POWER_DPS] = 5678
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "timer": 1,
@@ -80,17 +80,3 @@ class TestSwitchV2(TuyaDeviceTestCase):
                 "current_power_w": 567.8,
             },
         )
-
-        self.dps[TIMER_DPS] = 0
-        self.dps[CURRENT_DPS] = None
-        self.dps[VOLTAGE_DPS] = None
-        self.dps[POWER_DPS] = None
-        self.assertCountEqual(
-            self.subject.device_state_attributes,
-            {
-                "timer": 0,
-                "current_a": None,
-                "voltage_v": None,
-                "current_power_w": None,
-            },
-        )

+ 2 - 2
tests/devices/test_tadiran_wind_heatpump.py

@@ -202,7 +202,7 @@ class TestTadiranWindHeatpump(TuyaDeviceTestCase):
         self.dps["107"] = True
         self.dps["108"] = False
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "unknown_101": 101,
@@ -210,7 +210,7 @@ class TestTadiranWindHeatpump(TuyaDeviceTestCase):
                 "unknown_103": 103,
                 "unknown_104": "unknown104",
                 "unknown_105": "unknown105",
-                "unknown_106": 120,
+                "unknown_106": 106,
                 "unknown_107": True,
                 "unknown_108": False,
             },

+ 1 - 1
tests/devices/test_wetair_wch750_heater.py

@@ -194,7 +194,7 @@ class TestWetairWCH750Heater(TuyaDeviceTestCase):
         self.dps[COUNTDOWN_DPS] = 20
         self.dps[UNKNOWN21_DPS] = 21
 
-        self.assertCountEqual(
+        self.assertDictEqual(
             self.subject.device_state_attributes,
             {
                 "timer": "1h",