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

Add more secondary entities to devices to expose more of their functionality.

Refactor tests for simple entities into more mixins to make adding tests easier.

There are still some timers on devices where the spec is unknown that haven't been separated out, and some of the definitions for timers that were separated out are potentially wrong.
Jason Rumney 4 лет назад
Родитель
Сommit
45c3a36ae6
95 измененных файлов с 2557 добавлено и 578 удалено
  1. 35 7
      custom_components/tuya_local/devices/README.md
  2. 11 0
      custom_components/tuya_local/devices/anko_fan.yaml
  3. 1 0
      custom_components/tuya_local/devices/arlec_fan.yaml
  4. 45 0
      custom_components/tuya_local/devices/awow_th213_thermostat.yaml
  5. 2 0
      custom_components/tuya_local/devices/beca_bhp6000_thermostat_c.yaml
  6. 2 0
      custom_components/tuya_local/devices/beca_bhp6000_thermostat_f.yaml
  7. 13 0
      custom_components/tuya_local/devices/beca_bht002_thermostat_c.yaml
  8. 13 0
      custom_components/tuya_local/devices/beca_bht6000_thermostat_c.yaml
  9. 13 0
      custom_components/tuya_local/devices/bwt_heatpump.yaml
  10. 1 0
      custom_components/tuya_local/devices/deta_fan.yaml
  11. 54 0
      custom_components/tuya_local/devices/eanons_humidifier.yaml
  12. 11 0
      custom_components/tuya_local/devices/eberg_qubo_q40hd_heatpump.yaml
  13. 1 0
      custom_components/tuya_local/devices/electriq_12wminv_heatpump.yaml
  14. 21 0
      custom_components/tuya_local/devices/electriq_cd12pw_dehumidifier.yaml
  15. 28 0
      custom_components/tuya_local/devices/electriq_cd20pro_dehumidifier.yaml
  16. 20 0
      custom_components/tuya_local/devices/electriq_cd25pro_dehumidifier.yaml
  17. 10 0
      custom_components/tuya_local/devices/electriq_desd9lw_dehumidifier.yaml
  18. 13 1
      custom_components/tuya_local/devices/eurom_600_heater.yaml
  19. 21 2
      custom_components/tuya_local/devices/gardenpac_heatpump.yaml
  20. 17 9
      custom_components/tuya_local/devices/goldair_dehumidifier.yaml
  21. 1 0
      custom_components/tuya_local/devices/goldair_fan.yaml
  22. 23 1
      custom_components/tuya_local/devices/goldair_geco_heater.yaml
  23. 23 0
      custom_components/tuya_local/devices/goldair_gpcv_heater.yaml
  24. 37 0
      custom_components/tuya_local/devices/goldair_gpph_heater.yaml
  25. 10 0
      custom_components/tuya_local/devices/greenwind_dehumidifier.yaml
  26. 2 0
      custom_components/tuya_local/devices/grid_connect_usb_double_power_point.yaml
  27. 140 15
      custom_components/tuya_local/devices/inkbird_itc306a_thermostat.yaml
  28. 22 0
      custom_components/tuya_local/devices/kogan_dehumidifier.yaml
  29. 11 0
      custom_components/tuya_local/devices/kogan_kahtp_heater.yaml
  30. 11 0
      custom_components/tuya_local/devices/kogan_kawfhtp_heater.yaml
  31. 13 0
      custom_components/tuya_local/devices/lexy_f501_fan.yaml
  32. 20 1
      custom_components/tuya_local/devices/madimack_heatpump.yaml
  33. 1 0
      custom_components/tuya_local/devices/moes_bht002_thermostat_c.yaml
  34. 13 0
      custom_components/tuya_local/devices/poolex_silverline_heatpump.yaml
  35. 14 1
      custom_components/tuya_local/devices/poolex_vertigo_heatpump.yaml
  36. 2 0
      custom_components/tuya_local/devices/purline_m100_heater.yaml
  37. 13 0
      custom_components/tuya_local/devices/remora_heatpump.yaml
  38. 38 0
      custom_components/tuya_local/devices/renpho_rp_ap001s.yaml
  39. 67 0
      custom_components/tuya_local/devices/saswell_c16_thermostat.yaml
  40. 29 2
      custom_components/tuya_local/devices/saswell_t29utk_thermostat.yaml
  41. 33 0
      custom_components/tuya_local/devices/smartplugv1.yaml
  42. 33 0
      custom_components/tuya_local/devices/smartplugv2.yaml
  43. 1 0
      custom_components/tuya_local/devices/stirling_fs140dc_fan.yaml
  44. 71 0
      custom_components/tuya_local/devices/wetair_wch750_heater.yaml
  45. 56 2
      custom_components/tuya_local/translations/en.json
  46. 0 204
      tests/devices/base_device_tests.py
  47. 5 2
      tests/devices/test_anko_fan.py
  48. 14 20
      tests/devices/test_arlec_fan.py
  49. 49 4
      tests/devices/test_awow_th213_thermostat.py
  50. 3 1
      tests/devices/test_beca_bhp6000_thermostat.py
  51. 20 3
      tests/devices/test_beca_bht002_thermostat.py
  52. 20 3
      tests/devices/test_beca_bht6000_thermostat.py
  53. 9 1
      tests/devices/test_bwt_heatpump.py
  54. 3 6
      tests/devices/test_deta_fan.py
  55. 48 6
      tests/devices/test_eanons_humidifier.py
  56. 3 1
      tests/devices/test_eberg_qubo_q40hd_heatpump.py
  57. 3 2
      tests/devices/test_electriq_12wminv_heatpump.py
  58. 29 3
      tests/devices/test_electriq_cd12_dehumidifier.py
  59. 47 10
      tests/devices/test_electriq_cd20_dehumidifier.py
  60. 30 8
      tests/devices/test_electriq_cd25_dehumidifier.py
  61. 17 3
      tests/devices/test_electriq_desd9lw_dehumidifier.py
  62. 9 1
      tests/devices/test_eurom_600_heater.py
  63. 23 3
      tests/devices/test_gardenpac_heatpump.py
  64. 45 45
      tests/devices/test_goldair_dehumidifier.py
  65. 3 2
      tests/devices/test_goldair_fan.py
  66. 15 3
      tests/devices/test_goldair_geco_heater.py
  67. 15 3
      tests/devices/test_goldair_gpcv_heater.py
  68. 32 30
      tests/devices/test_goldair_gpph_heater.py
  69. 39 113
      tests/devices/test_grid_connect_double_power_point.py
  70. 96 11
      tests/devices/test_inkbird_itc306a_thermostat.py
  71. 25 3
      tests/devices/test_kogan_dehumidifier.py
  72. 5 3
      tests/devices/test_kogan_kahtp_heater.py
  73. 5 3
      tests/devices/test_kogan_kawfhtp_heater.py
  74. 7 7
      tests/devices/test_lexy_f501_fan.py
  75. 22 3
      tests/devices/test_madimack_heatpump.py
  76. 2 1
      tests/devices/test_moes_bht002_thermostat.py
  77. 9 1
      tests/devices/test_poolex_silverline_heatpump.py
  78. 9 1
      tests/devices/test_poolex_vertigo_heatpump.py
  79. 2 1
      tests/devices/test_purline_m100_heater.py
  80. 9 1
      tests/devices/test_remora_heatpump.py
  81. 32 10
      tests/devices/test_renpho_rp_ap001s.py
  82. 61 4
      tests/devices/test_saswell_c16_thermostat.py
  83. 24 2
      tests/devices/test_saswell_t29utk_thermostat.py
  84. 28 3
      tests/devices/test_smartplugv1.py
  85. 28 3
      tests/devices/test_smartplugv2.py
  86. 2 1
      tests/devices/test_stirling_fs140dc_fan.py
  87. 38 2
      tests/devices/test_wetair_wch750_heater.py
  88. 65 0
      tests/mixins/binary_sensor.py
  89. 66 0
      tests/mixins/light.py
  90. 48 0
      tests/mixins/lock.py
  91. 115 0
      tests/mixins/number.py
  92. 74 0
      tests/mixins/select.py
  93. 85 0
      tests/mixins/sensor.py
  94. 198 0
      tests/mixins/switch.py
  95. 5 0
      tests/test_config_flow.py

+ 35 - 7
custom_components/tuya_local/devices/README.md

@@ -99,8 +99,19 @@ The value of this should indicated what to use instead.
 
 //Optional.//
 
-For some entity types, a `class` can be set, for example `switch` entities
-can have a class of `outlet`.  This may slightly alter the UI behaviour.
+For some entity types, a device `class` can be set, for example `switch`
+entities can have a class of `outlet`.  This may slightly alter the UI
+behaviour. 
+For most entities, it will alter the default icon, and for binary sensors
+also the state that off and on values translate to in the UI.
+
+### `category`
+
+//Optional.//
+
+This specifies the `entity category` of the entity.  Entities can be categorized
+as `config` or `diagnostic` to restrict where they appear automatically in
+Home Assistant.
 
 ### `dps`
 
@@ -119,7 +130,15 @@ it will inherit the name at the top level. This is mostly useful for
 overriding the name of secondary entities to give more information
 about the purpose of the entity, as the generic type with the top level
 name may not be sufficient to describe the function.
- 
+
+### `mode`
+
+//Optional.  For number entities, default="auto", for others, None
+
+For number entities, this can be used to force `slider` or `box` as the
+input method.  The default `auto` uses a slider if the range is small enough,
+or a box otherwise.
+
 ## DPS configuration
  
 ### `id`
@@ -190,12 +209,21 @@ Home Assistant UI.  This can also be set in a `mapping` or `conditions` block.
 
 ### `unit`
 
-//Optional. default="C" for temperature dps//
+//Optional. default="C" for temperature dps on climate devices, None for sensors.//
 
 For temperature dps, some devices will use Fahrenhiet.  This needs to be
-indicated back to HomeAssistant by defining `unit` as "F".  In future `unit`
-will also be used for other sensor types, with a bigger range of possible
-values.
+indicated back to HomeAssistant by defining `unit` as "F".  For sensor 
+entities, see the HomeAssistant developer documentation for the full list
+of possible units (C and F are automatically translated to their Unicode 
+equivalents, other units are currently ASCII so can be easily entered directly).
+
+### `class`
+
+//Optional.  default=None.//
+
+For sensors, this sets the state class of the sensor (measurement, total
+or total_increasing)
+
 
 ## Mapping Rules
 

+ 11 - 0
custom_components/tuya_local/devices/anko_fan.yaml

@@ -34,3 +34,14 @@ primary_entity:
     - id: 6
       type: integer
       name: timer
+secondary_entities:
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 6
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 9

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

@@ -32,6 +32,7 @@ primary_entity:
 secondary_entities:
   - entity: select
     name: timer
+    category: config
     dps:
       - id: 103
         name: option

+ 45 - 0
custom_components/tuya_local/devices/awow_th213_thermostat.yaml

@@ -92,6 +92,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 6
         type: boolean
@@ -101,3 +102,47 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: sensor
+    name: External Temperature
+    class: temperature
+    dps:
+      - id: 101
+        type: integer
+        name: sensor
+        class: measurement
+        unit: C
+        readonly: true
+  - entity: select
+    name: Temperature Sensor
+    category: config
+    dps:
+      - id: 102
+        type: integer
+        name: option
+        mapping:
+          - dps_val: 0
+            value: Internal
+          - dps_val: 1
+            value: External
+          - dps_val: 2
+            value: Both
+  - entity: number
+    name: Calibration Offset
+    category: config
+    dps:
+      - id: 103
+        type: integer
+        name: value
+        range:
+          min: -9
+          max: 9
+  - entity: number
+    name: Calibration Swing
+    category: config
+    dps:
+      - id: 104
+        type: integer
+        name: value
+        range:
+          min: 1
+          max: 9

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

@@ -56,6 +56,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 7
         name: lock
@@ -67,6 +68,7 @@ secondary_entities:
             icon: "mdi:hand-back-right"
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 1
         name: switch

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

@@ -57,6 +57,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 7
         name: lock
@@ -68,6 +69,7 @@ secondary_entities:
             icon: "mdi:hand-back-right"
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 1
         name: switch

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

@@ -60,6 +60,7 @@ primary_entity:
 secondary_entities:
   - entity: light
     name: Display
+    category: config
     deprecated: climate hvac_mode
     dps:
       - id: 1
@@ -72,6 +73,7 @@ secondary_entities:
             icon: "mdi:led-off"
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 6
         type: boolean
@@ -81,3 +83,14 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: sensor
+    name: External Temperature
+    class: temperature
+    dps:
+      - id: 102
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        mapping:
+          - scale: 2

+ 13 - 0
custom_components/tuya_local/devices/beca_bht6000_thermostat_c.yaml

@@ -64,6 +64,7 @@ secondary_entities:
   - entity: light
     deprecated: climate hvac_mode
     name: Display
+    category: config
     dps:
       - id: 1
         type: boolean
@@ -75,6 +76,7 @@ secondary_entities:
             icon: "mdi:led-off"
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 6
         type: boolean
@@ -84,3 +86,14 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: sensor
+    name: External Temperature
+    class: temperature
+    dps:
+      - id: 102
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        mapping:
+          - scale: 2

+ 13 - 0
custom_components/tuya_local/devices/bwt_heatpump.yaml

@@ -51,3 +51,16 @@ primary_entity:
           value: "Water Flow Protection"
           icon: "mdi:water-pump-off"
           icon_priority: 2
+secondary_entities:
+  - entity: binary_sensor
+    class: problem
+    name: Water Flow
+    category: diagnostic
+    dps:
+      - id: 9
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

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

@@ -27,6 +27,7 @@ secondary_entities:
         name: timer
   - entity: switch
     name: Master
+    category: config
     dps:
       - id: 101
         type: boolean

+ 54 - 0
custom_components/tuya_local/devices/eanons_humidifier.yaml

@@ -123,3 +123,57 @@ secondary_entities:
       - id: 22
         name: switch
         type: boolean
+  - entity: select
+    name: Timer
+    category: config
+    dps:
+      - id: 3
+        name: option
+        type: string
+        mapping:
+          - dps_val: "cancel"
+            value: "Off"
+          - dps_val: "1"
+            value: "1 hour"
+          - dps_val: "2"
+            value: "2 hours"
+          - dps_val: "3"
+            value: "3 hours"
+          - dps_val: "4"
+            value: "4 hours"
+          - dps_val: "5"
+            value: "5 hours"
+          - dps_val: "6"
+            value: "6 hours"
+          - dps_val: "7"
+            value: "7 hours"
+          - dps_val: "8"
+            value: "8 hours"
+          - dps_val: "9"
+            value: "9 hours"
+          - dps_val: "10"
+            value: "10 hours"
+          - dps_val: "11"
+            value: "11 hours"
+          - dps_val: "12"
+            value: "12 hours"
+  - entity: sensor
+    name: Timer
+    category: diagnostic
+    dps:
+      - id: 4
+        name: sensor
+        type: integer
+        unit: min
+  - entity: binary_sensor
+    name: Tank
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 9
+        name: sensor
+        type: bitfield
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 11 - 0
custom_components/tuya_local/devices/eberg_qubo_q40hd_heatpump.yaml

@@ -123,3 +123,14 @@ primary_entity:
               value: "off"
             - dps_val: True
               value: idle
+secondary_entities:
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 22
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 24

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

@@ -157,6 +157,7 @@ secondary_entities:
         type: boolean
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 104
         name: switch

+ 21 - 0
custom_components/tuya_local/devices/electriq_cd12pw_dehumidifier.yaml

@@ -42,6 +42,7 @@ primary_entity:
 secondary_entities:
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 101
         type: boolean
@@ -51,3 +52,23 @@ secondary_entities:
             icon: "mdi:led-on"
           - dps_val: false
             icon: "mdi:led-off"
+  - entity: sensor
+    class: humidity
+    name: Current Humidity
+    dps:
+      - id: 3
+        type: integer
+        name: sensor
+        unit: "%"
+        class: measurement
+        readonly: true
+  - entity: sensor
+    class: temperature
+    name: Current Temperature
+    dps:
+      - id: 103
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        readonly: true

+ 28 - 0
custom_components/tuya_local/devices/electriq_cd20pro_dehumidifier.yaml

@@ -73,6 +73,7 @@ secondary_entities:
         type: boolean
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 101
         type: boolean
@@ -82,3 +83,30 @@ secondary_entities:
             icon: "mdi:led-on"
           - dps_val: false
             icon: "mdi:led-off"
+  - entity: switch
+    name: Ionizer
+    icon: mdi:creation
+    dps:
+      - id: 5
+        name: switch
+        type: boolean
+  - entity: sensor
+    class: humidity
+    name: Current Humidity
+    dps:
+      - id: 3
+        type: integer
+        name: sensor
+        unit: "%"
+        class: measurement
+        readonly: true
+  - entity: sensor
+    class: temperature
+    name: Current Temperature
+    dps:
+      - id: 103
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        readonly: true

+ 20 - 0
custom_components/tuya_local/devices/electriq_cd25pro_dehumidifier.yaml

@@ -87,3 +87,23 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: sensor
+    class: humidity
+    name: Current Humidity
+    dps:
+      - id: 3
+        type: integer
+        name: sensor
+        unit: "%"
+        class: measurement
+        readonly: true
+  - entity: sensor
+    class: temperature
+    name: Current Temperature
+    dps:
+      - id: 103
+        type: integer
+        name: sensor
+        unit: C
+        class: measurement
+        readonly: true

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

@@ -119,3 +119,13 @@ secondary_entities:
       - id: 12
         name: switch
         type: boolean
+  - entity: sensor
+    name: Current Humidity
+    class: humidity
+    dps:
+      - id: 6
+        type: integer
+        name: sensor
+        class: measurement
+        unit: "%"
+        readonly: true

+ 13 - 1
custom_components/tuya_local/devices/eurom_600_heater.yaml

@@ -32,4 +32,16 @@ primary_entity:
           value: "OK"
       readonly: true
       name: error
-      
+secondary_entities:
+  - entity: binary_sensor
+    name: Error
+    category: diagnostic
+    class: problem
+    dps:
+      - id: 6
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 21 - 2
custom_components/tuya_local/devices/gardenpac_heatpump.yaml

@@ -31,9 +31,17 @@ primary_entity:
       type: integer
       readonly: true
     - id: 105
-      name: operating_mode
+      name: hvac_action
       type: string
       readonly: true
+      mapping:
+        - dps_val: warm
+          constraint: hvac_mode
+          conditions:
+            - dps_val: false
+              value: "off"
+            - dps_val: true
+              value: idle
     - id: 106
       name: temperature
       type: integer
@@ -67,4 +75,15 @@ primary_entity:
           value: Silent
         - dps_val: true
           value: Smart
-        
+secondary_entities:
+  - entity: sensor
+    name: Power Level
+    class: power_factor
+    category: diagnostic
+    dps:
+      - id: 104
+        name: sensor
+        type: integer
+        unit: "%"
+        class: measurement
+        readonly: true

+ 17 - 9
custom_components/tuya_local/devices/goldair_dehumidifier.yaml

@@ -55,9 +55,6 @@ primary_entity:
         - dps_val: 0
           value: "OK"
       readonly: true
-    - id: 12
-      type: string
-      name: unknown_12
     - id: 101
       type: boolean
       name: unknown_101
@@ -199,9 +196,6 @@ secondary_entities:
           - dps_val: 0
             value: "OK"
         readonly: true
-      - id: 12
-        type: string
-        name: unknown_12
       - id: 101
         type: boolean
         name: unknown_101
@@ -224,6 +218,7 @@ secondary_entities:
             readonly: true
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 102
         type: boolean
@@ -237,6 +232,7 @@ secondary_entities:
             icon: "mdi:led-off"
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 7
         type: boolean
@@ -276,18 +272,30 @@ secondary_entities:
   - entity: binary_sensor
     name: Tank
     class: problem
+    category: diagnostic
     dps:
       - id: 11
         type: bitfield
         name: sensor
         mapping:
-          - dps_val: 8
-            value: true
-          - value: false
+          - dps_val: 0
+            value: false
+          - value: true
   - entity: binary_sensor
     name: defrost
     class: cold
+    category: diagnostic
     dps:
       - id: 105
         type: boolean
         name: sensor
+  - entity: number
+    name: timer
+    category: config
+    dps:
+      - id: 12
+        name: value
+        type: integer
+        range:
+          min: 0
+          max: 24

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

@@ -122,6 +122,7 @@ secondary_entities:
         name: timer
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 101
         type: boolean

+ 23 - 1
custom_components/tuya_local/devices/goldair_geco_heater.yaml

@@ -36,6 +36,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: "Child Lock"
+    category: config
     dps:
       - id: 2
         type: boolean
@@ -45,4 +46,25 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
-
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 5
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 24
+  - entity: binary_sensor
+    name: Error
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 6
+        name: sensor
+        type: bitfield
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 23 - 0
custom_components/tuya_local/devices/goldair_gpcv_heater.yaml

@@ -44,6 +44,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: "Child Lock"
+    category: config
     dps:
       - id: 2
         type: boolean
@@ -53,3 +54,25 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 5
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 24
+  - entity: binary_sensor
+    name: Error
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 6
+        name: sensor
+        type: bitfield
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 37 - 0
custom_components/tuya_local/devices/goldair_gpph_heater.yaml

@@ -121,6 +121,7 @@ primary_entity:
 secondary_entities:
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 104
         type: boolean
@@ -132,6 +133,7 @@ secondary_entities:
         name: switch
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 6
         type: boolean
@@ -143,6 +145,7 @@ secondary_entities:
             icon: "mdi:hand-back-right"
   - entity: number
     name: Timer
+    category: config
     dps:
       - id: 102
         type: integer
@@ -152,3 +155,37 @@ secondary_entities:
           max: 1440
         mapping:
           - step: 60
+  - entity: sensor
+    name: Power Level
+    class: power_factor
+    category: diagnostic
+    dps:
+      - id: 101
+        type: string
+        name: sensor
+        unit: "%"
+        mapping:
+          - dps_val: "stop"
+            value: 0
+          - dps_val: "1"
+            value: 20
+          - dps_val: "2"
+            value: 40
+          - dps_val: "3"
+            value: 60
+          - dps_val: "4"
+            value: 80
+          - dps_val: "5"
+            value: 100
+  - entity: binary_sensor
+    name: Error
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 12
+        name: sensor
+        type: bitfield
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

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

@@ -30,4 +30,14 @@ primary_entity:
     - id: 18
       name: current_humidity
       type: integer
+secondary_entities:
+  - entity: sensor
+    name: Current Humidity
+    class: humidity
+    dps:
+      - id: 18
+        type: integer
+        name: sensor
+        unit: "%"
+        class: measurement
 

+ 2 - 0
custom_components/tuya_local/devices/grid_connect_usb_double_power_point.yaml

@@ -7,6 +7,7 @@
 name: Grid Connnect power metered double outlet with USB
 primary_entity:
   entity: switch
+  category: config
   name: Master
   class: outlet
   dps:
@@ -62,6 +63,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 40
         name: lock

+ 140 - 15
custom_components/tuya_local/devices/inkbird_itc306a_thermostat.yaml

@@ -35,18 +35,17 @@ primary_entity:
     - id: 106
       type: integer
       name: target_temp_low
+      range:
+        min: 0
+        max: 450
       mapping:
         - scale: 10
           constraint: temperature_unit
           conditions:
-            - dps_val: "C"
+            - dps_val: F
               range:
-                min: 200
-                max: 350
-            - dps_val: "F"
-              range:
-                min: 680
-                max: 950
+                min: 320
+                max: 1130
     - id: 108
       type: integer
       name: heat_time_alarm_threshold_hours
@@ -84,28 +83,29 @@ primary_entity:
     - id: 114
       type: integer
       name: target_temp_high
+      range:
+        min: 0
+        max: 450
       mapping:
         - scale: 10
           constraint: temperature_unit
           conditions:
-            - dps_val: "C"
-              range:
-                min: 200
-                max: 350
-            - dps_val: "F"
+            - dps_val: F
               range:
-                min: 680
-                max: 950
+                min: 320
+                max: 1130
     - id: 115
       type: boolean
-      name: switch_state
+      name: hvac_action
       mapping:
         - dps_val: true
           icon: "mdi:thermometer"
           icon_priority: 5
+          value: heating
         - dps_val: false
           icon: "mdi:thermometer-off"
           icon_priority: 4
+          value: idle
     - id: 116
       type: integer
       name: current_temperature_f
@@ -123,3 +123,128 @@ primary_entity:
     - id: 120
       type: boolean
       name: unknown_120
+secondary_entities:
+  - entity: number
+    category: config
+    name: Calibration Offset
+    dps:
+      - id: 102
+        name: value
+        type: integer
+        range:
+          min: -99
+          max: 99
+        mapping:
+          - scale: 10
+            constraint: unit
+            conditions:
+              - dps_val: F
+                range:
+                  min: -150
+                  max: 150
+      - id: 101
+        name: unit
+        type: string
+        hidden: true
+  - entity: number
+    name: Continuous Heat Hours
+    category: config
+    dps:
+      - id: 108
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 96
+  - entity: number
+    name: High Temperature Limit
+    category: config
+    dps:
+      - id: 109
+        name: value
+        type: integer
+        range:
+          min: -400
+          max: 1000
+        mapping:
+          - scale: 10
+            constraint: unit
+            conditions:
+              - dps_val: F
+                range:
+                  min: -400
+                  max: 2120
+      - id: 101
+        name: unit
+        type: string
+        hidden: true
+  - entity: number
+    name: Low Temperature Limit
+    category: config
+    dps:
+      - id: 110
+        name: value
+        type: integer
+        range:
+          min: -400
+          max: 1000
+        mapping:
+          - scale: 10
+            constraint: unit
+            conditions:
+              - dps_val: F
+                range:
+                  min: -400
+                  max: 2120
+      - id: 101
+        name: unit
+        type: string
+        hidden: true
+  - entity: select
+    category: config
+    name: Temperature Unit
+    dps:
+      - id: 101
+        name: option
+        type: string
+        mapping:
+          - dps_val: C
+            value: Celsius
+          - dps_val: F
+            value: Fahrenheit
+  - entity: binary_sensor
+    class: heat
+    category: diagnostic
+    name: High Temperature
+    dps:
+      - id: 111
+        type: boolean
+        name: sensor
+  - entity: binary_sensor
+    class: cold
+    category: diagnostic
+    name: Low Temperature
+    dps:
+      - id: 112
+        type: boolean
+        name: sensor
+  - entity: binary_sensor
+    class: problem
+    category: diagnostic
+    name: Continuous Heat
+    dps:
+      - id: 113
+        type: boolean
+        name: sensor
+  - entity: binary_sensor
+    name: Error
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 12
+        name: sensor
+        type: bitfield
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 22 - 0
custom_components/tuya_local/devices/kogan_dehumidifier.yaml

@@ -76,3 +76,25 @@ secondary_entities:
       - id: 8
         name: oscillate
         type: boolean
+  - entity: sensor
+    class: humidity
+    name: Current Humidity
+    dps:
+      - id: 3
+        type: integer
+        name: sensor
+        class: measurement
+        unit: "%"
+  - entity: binary_sensor
+    class: problem
+    name: Tank
+    dps:
+      - id: 11
+        name: sensor
+        type: integer
+        readonly: true
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true
+     

+ 11 - 0
custom_components/tuya_local/devices/kogan_kahtp_heater.yaml

@@ -37,6 +37,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: "Child Lock"
+    category: config
     dps:
       - id: 6
         type: boolean
@@ -46,3 +47,13 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 8
+        name: value
+        type: integer
+        range:
+          min: 0
+          max: 24

+ 11 - 0
custom_components/tuya_local/devices/kogan_kawfhtp_heater.yaml

@@ -36,6 +36,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: "Child Lock"
+    category: config
     dps:
       - id: 2
         type: boolean
@@ -45,3 +46,13 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 5
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 24

+ 13 - 0
custom_components/tuya_local/devices/lexy_f501_fan.yaml

@@ -48,6 +48,7 @@ primary_entity:
         - scale: 0.15
 secondary_entities:
   - entity: light
+    category: config
     dps:
       - id: 9
         name: switch
@@ -59,6 +60,7 @@ secondary_entities:
             icon: "mdi:led-off"
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 16
         name: lock
@@ -70,6 +72,7 @@ secondary_entities:
             icon: "mdi:hand-back-right"
   - entity: switch
     name: Sound
+    category: config
     dps:
       - id: 17
         name: switch
@@ -79,3 +82,13 @@ secondary_entities:
             icon: "mdi:volume-high"
           - dps_val: false
             icon: "mdi:volume-mute"
+  - entity: number
+    name: Timer
+    category: config
+    dps:
+      - id: 6
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 7

+ 20 - 1
custom_components/tuya_local/devices/madimack_heatpump.yaml

@@ -31,9 +31,17 @@ primary_entity:
       type: integer
       readonly: true
     - id: 105
-      name: operating_mode
+      name: hvac_action
       type: string
       readonly: true
+      mapping:
+        - dps_val: "warm"
+          constraint: hvac_mode
+          conditions:
+            - dps_val: false
+              value: "off"
+            - dps_val: true
+              value: idle
     - id: 106
       name: temperature
       type: integer
@@ -112,3 +120,14 @@ primary_entity:
     - id: 140
       name: unknown_140
       type: string
+secondary_entities:
+  - entity: sensor
+    category: disagnostic
+    name: Power Level
+    class: power_factor
+    dps:
+      - id: 104
+        type: integer
+        name: sensor
+        unit: "%"
+        readonly: true

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

@@ -55,6 +55,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 6
         type: boolean

+ 13 - 0
custom_components/tuya_local/devices/poolex_silverline_heatpump.yaml

@@ -45,3 +45,16 @@ primary_entity:
           value: "Water Flow Protection"
           icon: "mdi:water-pump-off"
           icon_priority: 2
+secondary_entities:
+  - entity: binary_sensor
+    name: Water Flow
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 13
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 14 - 1
custom_components/tuya_local/devices/poolex_vertigo_heatpump.yaml

@@ -42,4 +42,17 @@ primary_entity:
         - dps_val: 4
           value: "Water Flow Protection"
           icon: "mdi:water-pump-off"
-          icon_priority: 2
+          icon_priority: 2
+secondary_entities:
+  - entity: binary_sensor
+    name: Water Flow
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 9
+        type: integer
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

+ 2 - 0
custom_components/tuya_local/devices/purline_m100_heater.yaml

@@ -66,6 +66,7 @@ primary_entity:
 secondary_entities:
   - entity: light
     name: Display
+    category: config
     dps:
       - id: 10
         type: boolean
@@ -79,6 +80,7 @@ secondary_entities:
         name: switch
   - entity: switch
     name: "Open Window Detector"
+    category: config
     class: switch
     dps:
       - id: 101

+ 13 - 0
custom_components/tuya_local/devices/remora_heatpump.yaml

@@ -49,3 +49,16 @@ primary_entity:
           value: "Water Flow Protection"
           icon: "mdi:water-pump-off"
           icon_priority: 2
+secondary_entities:
+  - entity: binary_sensor
+    name: Water Flow
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 9
+        name: sensor
+        type: integer
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true

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

@@ -41,6 +41,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 7
         name: lock
@@ -52,6 +53,7 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
   - entity: light
     name: AQ indicator
+    category: config
     class: switch
     dps:
       - id: 8
@@ -70,3 +72,39 @@ secondary_entities:
       - id: 101
         name: switch
         type: boolean
+  - entity: sensor
+    name: Air Quality
+    class: aqi
+    category: diagnostic
+    dps:
+      - id: 22
+        type: string
+        name: sensor
+  - entity: sensor
+    name: Prefilter Life
+    category: diagnostic
+    dps:
+      - id: 102
+        type: integer
+        name: sensor
+  - entity: sensor
+    name: Charcoal Filter Life
+    category: diagnostic
+    dps:
+      - id: 103
+        type: integer
+        name: sensor
+  - entity: sensor
+    name: Active Filter Life
+    category: diagnostic
+    dps:
+      - id: 104
+        type: integer
+        name: sensor
+  - entity: sensor
+    name: HEPA Filter Life
+    category: diagnostic
+    dps:
+      - id: 105
+        type: integer
+        name: sensor

+ 67 - 0
custom_components/tuya_local/devices/saswell_c16_thermostat.yaml

@@ -101,6 +101,7 @@ primary_entity:
 secondary_entities:
   - entity: lock
     name: Child Lock
+    category: config
     dps:
       - id: 11
         name: lock
@@ -110,3 +111,69 @@ secondary_entities:
             icon: "mdi:hand-back-right-off"
           - dps_val: false
             icon: "mdi:hand-back-right"
+  - entity: number
+    name: Floor Temperature Limit
+    category: config
+    dps:
+      - id: 6
+        name: value
+        type: integer
+        range:
+          min: 200
+          max: 500
+        mapping:
+          - scale: 10
+            step: 5
+  - entity: select
+    name: Installation
+    category: config
+    dps:
+      - id: 7
+        name: option
+        type: boolean
+        mapping:
+          - dps_val: true
+            value: Office
+          - dps_val: false
+            value: Home
+  - entity: sensor
+    name: Floor Temperature
+    class: temperature
+    category: diagnostic
+    dps:
+      - id: 8
+        name: sensor
+        type: integer
+        class: measurement
+        unit: C
+        mapping:
+          - scale: 10
+  - entity: switch
+    name: Adaptive
+    category: config
+    dps:
+      - id: 10
+        name: switch
+        type: boolean
+  - entity: select
+    name: Schedule
+    category: config
+    dps:
+      - id: 12
+        name: option
+        type: string
+        mapping:
+          - dps_val: "5_1_1"
+            value: "Weekdays+Sat+Sun"
+          - dps_val: "7"
+            value: "Daily"
+  - entity: number
+    name: Power Rating
+    category: config
+    dps:
+      - id: 22
+        name: value
+        type: integer
+        range:
+          min: 0
+          max: 3500

+ 29 - 2
custom_components/tuya_local/devices/saswell_t29utk_thermostat.yaml

@@ -125,5 +125,32 @@ primary_entity:
     - id: 117
       name: current_temperature_f
       type: integer
-
-      
+secondary_entities:
+  - entity: select
+    name: Temperature Unit
+    category: config
+    dps:
+      - id: 19
+        name: option
+        type: string
+        mapping:
+          - dps_val: F
+            value: Fahrenheit
+          - dps_val: C
+            value: Celsius
+  - entity: select
+    name: Configuration
+    category: config
+    dps:
+      - id: 112
+        name: option
+        type: string
+        mapping:
+          - dps_val: "1"
+            value: "cooling"
+          - dps_val: "2"
+            value: "heating"
+          - dps_val: "3"
+            value: "heat/cool"
+          - dps_val: "5"
+            value: "heatpump"

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

@@ -28,3 +28,36 @@ primary_entity:
       readonly: true
       mapping:
         - scale: 10
+secondary_entities:
+  - entity: sensor
+    category: diagnostic
+    class: voltage
+    name: Voltage
+    dps:
+      - id: 6
+        name: sensor
+        type: integer
+        class: measurement
+        unit: V
+        mapping:
+          - scale: 10
+  - entity: sensor
+    category: diagnostic
+    class: current
+    name: Current
+    dps:
+      - id: 4
+        name: sensor
+        type: integer
+        class: measurement
+        unit: mA
+  - entity: number
+    category: config
+    name: Timer
+    dps:
+      - id: 2
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 1440

+ 33 - 0
custom_components/tuya_local/devices/smartplugv2.yaml

@@ -28,3 +28,36 @@ primary_entity:
       read-only: true
       mapping:
         - scale: 10
+secondary_entities:
+  - entity: sensor
+    category: diagnostic
+    class: voltage
+    name: Voltage
+    dps:
+      - id: 20
+        name: sensor
+        type: integer
+        class: measurement
+        unit: V
+        mapping:
+          - scale: 10
+  - entity: sensor
+    category: diagnostic
+    class: current
+    name: Current
+    dps:
+      - id: 18
+        name: sensor
+        type: integer
+        class: measurement
+        unit: mA
+  - entity: number
+    category: config
+    name: Timer
+    dps:
+      - id: 9
+        type: integer
+        name: value
+        range:
+          min: 0
+          max: 1440

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

@@ -29,6 +29,7 @@ primary_entity:
 secondary_entities:
   - entity: select
     name: timer
+    category: config
     dps:
       - id: 22
         name: option

+ 71 - 0
custom_components/tuya_local/devices/wetair_wch750_heater.yaml

@@ -63,6 +63,7 @@ primary_entity:
       name: unknown_21
 secondary_entities:
   - entity: light
+    category: config
     name: Display
     dps:
       - id: 101
@@ -71,10 +72,80 @@ secondary_entities:
         mapping:
           - dps_val: level0
             value: 0
+            step: 85
           - dps_val: level1
             value: 85
+            step: 85
           - dps_val: level2
             value: 170
+            step: 85
           - dps_val: level3
             value: 255
+            step: 85
           - step: 85
+  - entity: select
+    name: Timer
+    category: config
+    dps:
+      - id: 19
+        type: string
+        name: option
+        mapping:
+          - dps_val: "0h"
+            value: "Off"
+          - dps_val: "1h"
+            value: "1 hour"
+          - dps_val: "2h"
+            value: "2 hours"
+          - dps_val: "3h"
+            value: "3 hours"
+          - dps_val: "4h"
+            value: "4 hours"
+          - dps_val: "5h"
+            value: "5 hours"
+          - dps_val: "6h"
+            value: "6 hours"
+          - dps_val: "7h"
+            value: "7 hours"
+          - dps_val: "8h"
+            value: "8 hours"
+          - dps_val: "9h"
+            value: "9 hours"
+          - dps_val: "10h"
+            value: "10 hours"
+          - dps_val: "11h"
+            value: "11 hours"
+          - dps_val: "12h"
+            value: "12 hours"
+          - dps_val: "13h"
+            value: "13 hours"
+          - dps_val: "14h"
+            value: "14 hours"
+          - dps_val: "15h"
+            value: "15 hours"
+          - dps_val: "16h"
+            value: "16 hours"
+          - dps_val: "17h"
+            value: "17 hours"
+          - dps_val: "18h"
+            value: "18 hours"
+          - dps_val: "19h"
+            value: "19 hours"
+          - dps_val: "20h"
+            value: "20 hours"
+          - dps_val: "21h"
+            value: "21 hours"
+          - dps_val: "22h"
+            value: "22 hours"
+          - dps_val: "23h"
+            value: "23 hours"
+          - dps_val: "24h"
+            value: "24 hours"          
+  - entity: sensor
+    category: diagnostic
+    name: Timer
+    dps:
+      - id: 20
+        type: integer
+        name: sensor
+        unit: min

+ 56 - 2
custom_components/tuya_local/translations/en.json

@@ -33,8 +33,12 @@
 		    "select": "Include a select entity",
 		    "sensor": "Include a sensor entity",
 		    "switch": "Include a switch entity",
-		    "binary_sensor_tank": "Include tank as a binary_sensor entity.",
+		    "binary_sensor_continuous_heat", "Include continuous heat alarm as a binary_sensor entity",
 		    "binary_sensor_defrost": "Include defrost as a binary_sensor entity.",
+		    "binary_sensor_high_temperature", "Include high temperature alarm as a binary_sensor entity",
+		    "binary_sensor_low_temperature", "Include low temperature alarm as a binary_sensor entity",
+		    "binary_sensor_tank": "Include tank as a binary_sensor entity",
+		    "binary_sensor_water_flow": "Include water flow warning as a binary_sensor entity",
 		    "climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
 		    "fan_intensity": "Include intensity as a fan entitiy",
 		    "light_aq_indicator": "Include AQ indicator as a light entity",
@@ -43,10 +47,33 @@
 		    "light_flame": "Include flame as a light entity",
 		    "light_uv_sterilization": "Include UV sterilization as a light entitiy",
 		    "lock_child_lock": "Include child lock as a lock entity",
+		    "number_calibration_offset": "Include calibration offset as a number entitiy",
+		    "number_calibration_swing": "Include calibration swing as a number entity",
+		    "number_continuous_heat_hours": "Include Continuous Heating Time as a number entity",
+		    "number_high_temperature_limit": "Include High Temperature Limit as a number entity",
+		    "number_low_temperature_limit": "Include Low Temperature Limit as a number entity",
+		    "number_floor_temperature_limit": "Include Floor Temperature Limit as a number entity",
+		    "number_power_rating": "Include Power Rating as a number entity",
 		    "number_timer": "Include timer as a number entity",
+		    "select_installation": "Include installation 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",
+		    "select_temperature_unit": "Include temerature unit as a select entity",
+		    "sensor_air_quality": "Include air quality as a sensor entity",
+		    "sensor_prefilter_life": "Include prefilter life as a sensor entity",
+		    "sensor_charcoal_filter_life": "Include charcoal filter life as a sensor entity",
+		    "sensor_active_filter_life": "Include active filter life as a sensor entity",
+		    "sensor_hepa_filter_life": "Include HEPA filter life as a sensor entity",
 		    "sensor_current_humidity": "Include current humidity as a sensor entity",
 		    "sensor_current_temperature": "Include current temperature as a sensor entity",
+		    "sensor_external_temperature": "Include external temperature as a sensor entity",
+		    "sensor_power_level": "Include power level as a sensor entity",
+		    "sensor_timer": "Include time remaining as a sensor entity",
+		    "sensor_floor_temperature": "Include floor temperature as a sensor entity",
+		    "sensor_current": "Include current as a sensor entity",
+		    "sensor_voltage": "Include voltage as a sensor entity",
+		    "switch_adaptive": "Include adaptive as a switch entity",
 		    "switch_air_clean": "Include air clean as a switch entity",
 		    "switch_ionizer": "Include ionizer as a switch entity",
 		    "switch_master": "Include master switch as a switch entity",
@@ -85,8 +112,12 @@
 		"select": "Include a select entity",
 		"sensor": "Include a sensor entity",
 		"switch": "Include a switch entity",
-		"binary_sensor_tank": "Include tank as a binary_sensor entity.",
+		"binary_sensor_continuous_heat", "Include continuous heat alarm as a binary_sensor entity",
 		"binary_sensor_defrost": "Include defrost as a binary_sensor entity.",
+		"binary_sensor_high_temperature", "Include high temperature alarm as a binary_sensor entity",
+		"binary_sensor_low_temperature", "Include low temperature alarm as a binary_sensor entity",
+		"binary_sensor_tank": "Include tank as a binary_sensor entity.",
+		"binary_sensor_water_flow": "Include water flow warning as a binary_sensor entity",
 		"climate_dehumidifier_as_climate": "Include a climate entity for the dehumidifier (deprecated, recommend using humidifier and fan instead)",
 		"fan_intensity": "Include intensity as a fan entitiy",
 		"light_aq_indicator": "Include AQ indicator as a light entity",
@@ -95,10 +126,33 @@
 		"light_flame": "Include flame as a light entity",
 		"light_uv_sterilization": "Include UV sterilization as a light entitiy",
 		"lock_child_lock": "Include child lock as a lock entity",
+		"number_calibration_offset": "Include calibration offset as a number entitiy",
+		"number_calibration_swing": "Include calibration swing as a number entity",
+		"number_continuous_heat_hours": "Include Continuous Heating Time as a number entity",
+		"number_high_temperature_limit": "Include High Temperature Limit as a number entity",
+		"number_low_temperature_limit": "Include Low Temperature Limit as a number entity",
+		"number_floor_temperature_limit": "Include Floor Temperature Limit as a number entity",
+		"number_power_rating": "Include Power Rating as a number entity",
 		"number_timer": "Include timer as a number entity",
+		"select_installation": "Include installation 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",
+		"select_temperature_unit": "Include temerature unit as a select entity",
+		"sensor_air_quality": "Include air quality as a sensor entity",
+		"sensor_prefilter_life": "Include prefilter life as a sensor entity",
+		"sensor_charcoal_filter_life": "Include charcoal filter life as a sensor entity",
+		"sensor_active_filter_life": "Include active filter life as a sensor entity",
+		"sensor_hepa_filter_life": "Include HEPA filter life as a sensor entity",
 		"sensor_current_humidity": "Include current humidity as a sensor entity",
 		"sensor_current_temperature": "Include current temperature as a sensor entity",
+		"sensor_external_temperature": "Include external temperature as a sensor entity",
+		"sensor_power_level": "Include power level as a sensor entity",
+		"sensor_timer": "Include time remaining as a sensor entity",
+		"sensor_floor_temperature": "Include floor temperature as a sensor entity",
+		"sensor_current": "Include current as a sensor entity",
+		"sensor_voltage": "Include voltage as a sensor entity",
+		"switch_adaptive": "Include adaptive as a switch entity",
 		"switch_air_clean": "Include air clean as a switch entity",
 		"switch_ionizer": "Include ionizer as a switch entity",
 		"switch_master": "Include master switch as a switch entity",

+ 0 - 204
tests/devices/base_device_tests.py

@@ -2,11 +2,6 @@ from unittest import IsolatedAsyncioTestCase
 from unittest.mock import AsyncMock, patch, PropertyMock
 from uuid import uuid4
 
-from homeassistant.components.light import COLOR_MODE_ONOFF
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.components.switch import DEVICE_CLASS_SWITCH
-from homeassistant.const import STATE_UNAVAILABLE
-
 from custom_components.tuya_local.generic.binary_sensor import TuyaLocalBinarySensor
 from custom_components.tuya_local.generic.climate import TuyaLocalClimate
 from custom_components.tuya_local.generic.fan import TuyaLocalFan
@@ -124,202 +119,3 @@ class TuyaDeviceTestCase(IsolatedAsyncioTestCase):
             await e.async_update()
             self.mock_device.async_refresh.assert_called_once()
             result.assert_awaited()
-
-
-# Mixins for common test functions
-
-
-class SwitchableTests:
-    def setUpSwitchable(self, dps, subject):
-        self.switch_dps = dps
-        self.switch_subject = subject
-
-    def test_switchable_is_on(self):
-        self.dps[self.switch_dps] = True
-        self.assertTrue(self.switch_subject.is_on)
-
-        self.dps[self.switch_dps] = False
-        self.assertFalse(self.switch_subject.is_on)
-
-        self.dps[self.switch_dps] = None
-        self.assertIsNone(self.switch_subject.is_on)
-
-    async def test_switchable_turn_on(self):
-        async with assert_device_properties_set(
-            self.switch_subject._device, {self.switch_dps: True}
-        ):
-            await self.switch_subject.async_turn_on()
-
-    async def test_switchable_turn_off(self):
-        async with assert_device_properties_set(
-            self.switch_subject._device, {self.switch_dps: False}
-        ):
-            await self.switch_subject.async_turn_off()
-
-    async def test_switchable_toggle(self):
-        self.dps[self.switch_dps] = False
-        async with assert_device_properties_set(
-            self.switch_subject._device, {self.switch_dps: True}
-        ):
-            await self.switch_subject.async_toggle()
-
-        self.dps[self.switch_dps] = True
-        async with assert_device_properties_set(
-            self.switch_subject._device, {self.switch_dps: False}
-        ):
-            await self.switch_subject.async_toggle()
-
-
-class BasicLightTests:
-    def setUpBasicLight(self, dps, subject):
-        self.basicLight = subject
-        self.basicLightDps = dps
-
-    def test_basic_light_supported_features(self):
-        self.assertEqual(self.basicLight.supported_features, 0)
-
-    def test_basic_light_supported_color_modes(self):
-        self.assertCountEqual(
-            self.basicLight.supported_color_modes,
-            [COLOR_MODE_ONOFF],
-        )
-
-    def test_basic_light_color_mode(self):
-        self.assertEqual(self.basicLight.color_mode, COLOR_MODE_ONOFF)
-
-    def test_light_has_no_brightness(self):
-        self.assertIsNone(self.basicLight.brightness)
-
-    def test_light_has_no_effects(self):
-        self.assertIsNone(self.basicLight.effect_list)
-        self.assertIsNone(self.basicLight.effect)
-
-    def test_basic_light_is_on(self):
-        self.dps[self.basicLightDps] = True
-        self.assertTrue(self.basicLight.is_on)
-        self.dps[self.basicLightDps] = False
-        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}
-        ):
-            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}
-        ):
-            await self.basicLight.async_turn_off()
-
-    async def test_basic_light_toggle_turns_on_when_it_was_off(self):
-        self.dps[self.basicLightDps] = False
-        async with assert_device_properties_set(
-            self.basicLight._device,
-            {self.basicLightDps: True},
-        ):
-            await self.basicLight.async_toggle()
-
-    async def test_basic_light_toggle_turns_off_when_it_was_on(self):
-        self.dps[self.basicLightDps] = True
-        async with assert_device_properties_set(
-            self.basicLight._device,
-            {self.basicLightDps: False},
-        ):
-            await self.basicLight.async_toggle()
-
-    def test_basic_light_state_attributes(self):
-        self.assertEqual(self.basicLight.device_state_attributes, {})
-
-
-class BasicLockTests:
-    def setUpBasicLock(self, dps, subject):
-        self.basicLock = subject
-        self.basicLockDps = dps
-
-    def test_basic_lock_state(self):
-        self.dps[self.basicLockDps] = True
-        self.assertEqual(self.basicLock.state, STATE_LOCKED)
-
-        self.dps[self.basicLockDps] = False
-        self.assertEqual(self.basicLock.state, STATE_UNLOCKED)
-
-        self.dps[self.basicLockDps] = None
-        self.assertEqual(self.basicLock.state, STATE_UNAVAILABLE)
-
-    def test_basic_lock_is_locked(self):
-        self.dps[self.basicLockDps] = True
-        self.assertTrue(self.basicLock.is_locked)
-
-        self.dps[self.basicLockDps] = False
-        self.assertFalse(self.basicLock.is_locked)
-
-        self.dps[self.basicLockDps] = None
-        self.assertFalse(self.basicLock.is_locked)
-
-    async def test_basic_lock_locks(self):
-        async with assert_device_properties_set(
-            self.basicLock._device,
-            {self.basicLockDps: True},
-        ):
-            await self.basicLock.async_lock()
-
-    async def test_basic_lock_unlocks(self):
-        async with assert_device_properties_set(
-            self.basicLock._device,
-            {self.basicLockDps: False},
-        ):
-            await self.basicLock.async_unlock()
-
-    def test_basic_lock_state_attributes(self):
-        self.assertEqual(self.basicLock.device_state_attributes, {})
-
-
-class BasicSwitchTests:
-    def setUpBasicSwitch(self, dps, subject):
-        self.basicSwitch = subject
-        self.basicSwitchDps = dps
-
-    def test_basic_switch_is_on(self):
-        self.dps[self.basicSwitchDps] = True
-        self.assertEqual(self.basicSwitch.is_on, True)
-
-        self.dps[self.basicSwitchDps] = False
-        self.assertEqual(self.basicSwitch.is_on, False)
-
-    async def test_basic_switch_turn_on(self):
-        async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: True}
-        ):
-            await self.basicSwitch.async_turn_on()
-
-    async def test_basic_switch_turn_off(self):
-        async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: False}
-        ):
-            await self.basicSwitch.async_turn_off()
-
-    async def test_basic_switch_toggle_turns_on_when_it_was_off(self):
-        self.dps[self.basicSwitchDps] = False
-
-        async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: True}
-        ):
-            await self.basicSwitch.async_toggle()
-
-    async def test_basic_switch_toggle_turns_off_when_it_was_on(self):
-        self.dps[self.basicSwitchDps] = True
-
-        async with assert_device_properties_set(
-            self.basicSwitch._device, {self.basicSwitchDps: False}
-        ):
-            await self.basicSwitch.async_toggle()
-
-    def test_basic_switch_class_is_switch(self):
-        self.assertEqual(self.basicSwitch.device_class, DEVICE_CLASS_SWITCH)
-
-    def test_basic_switch_has_no_power_monitoring(self):
-        self.assertIsNone(self.basicSwitch.current_power_w)
-
-    def test_basic_switch_state_attributes(self):
-        self.assertEqual(self.basicSwitch.device_state_attributes, {})

+ 5 - 2
tests/devices/test_anko_fan.py

@@ -6,7 +6,9 @@ from homeassistant.components.fan import (
 
 from ..const import ANKO_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import SwitchableTests, TuyaDeviceTestCase
+from ..mixins.number import BasicNumberTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 PRESET_DPS = "2"
@@ -15,13 +17,14 @@ OSCILLATE_DPS = "4"
 TIMER_DPS = "6"
 
 
-class TestAnkoFan(SwitchableTests, TuyaDeviceTestCase):
+class TestAnkoFan(SwitchableTests, BasicNumberTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("anko_fan.yaml", ANKO_FAN_PAYLOAD)
         self.subject = self.entities["fan"]
         self.setUpSwitchable(SWITCH_DPS, self.subject)
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=9)
 
     def test_supported_features(self):
         self.assertEqual(

+ 14 - 20
tests/devices/test_arlec_fan.py

@@ -10,7 +10,9 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import ARLEC_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import SwitchableTests, TuyaDeviceTestCase
+from ..mixins.select import BasicSelectTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 SPEED_DPS = "3"
@@ -19,7 +21,7 @@ PRESET_DPS = "102"
 TIMER_DPS = "103"
 
 
-class TestArlecFan(SwitchableTests, TuyaDeviceTestCase):
+class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
@@ -27,6 +29,16 @@ class TestArlecFan(SwitchableTests, TuyaDeviceTestCase):
         self.subject = self.entities["fan"]
         self.timer = self.entities["select_timer"]
         self.setUpSwitchable(SWITCH_DPS, self.subject)
+        self.setUpBasicSelect(
+            TIMER_DPS,
+            self.entities["select_timer"],
+            {
+                "off": "Off",
+                "2hour": "2 hours",
+                "4hour": "4 hours",
+                "8hour": "8 hours",
+            },
+        )
 
     def test_supported_features(self):
         self.assertEqual(
@@ -109,21 +121,3 @@ class TestArlecFan(SwitchableTests, TuyaDeviceTestCase):
     def test_device_state_attributes(self):
         self.dps[TIMER_DPS] = "2hour"
         self.assertEqual(self.subject.device_state_attributes, {"timer": "2hour"})
-        self.assertEqual(self.timer.device_state_attributes, {})
-
-    def test_timer_options(self):
-        self.assertCountEqual(
-            self.timer.options,
-            ["Off", "2 hours", "4 hours", "8 hours"],
-        )
-
-    def test_timer_current_option(self):
-        self.dps[TIMER_DPS] = "2hour"
-        self.assertEqual(self.timer.current_option, "2 hours")
-
-    async def test_select_option(self):
-        async with assert_device_properties_set(
-            self.timer._device,
-            {TIMER_DPS: "4hour"},
-        ):
-            await self.timer.async_select_option("4 hours")

+ 49 - 4
tests/devices/test_awow_th213_thermostat.py

@@ -7,12 +7,19 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import (
+    DEVICE_CLASS_TEMPERATURE,
+    STATE_UNAVAILABLE,
+    TEMP_CELSIUS,
+)
 
 from ..const import TH213_THERMOSTAT_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import MultiNumberTests
+from ..mixins.select import BasicSelectTests
+from ..mixins.sensor import BasicSensorTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -30,13 +37,51 @@ UNKNOWN108_DPS = "108"
 UNKNOWN110_DPS = "110"
 
 
-class TestAwowTH213Thermostat(BasicLockTests, TuyaDeviceTestCase):
+class TestAwowTH213Thermostat(
+    BasicLockTests,
+    BasicSelectTests,
+    BasicSensorTests,
+    MultiNumberTests,
+    TuyaDeviceTestCase,
+):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("awow_th213_thermostat.yaml", TH213_THERMOSTAT_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicSensor(
+            EXTERNTEMP_DPS,
+            self.entities.get("sensor_external_temperature"),
+            unit=TEMP_CELSIUS,
+            device_class=DEVICE_CLASS_TEMPERATURE,
+            state_class="measurement",
+        )
+        self.setUpBasicSelect(
+            SENSOR_DPS,
+            self.entities.get("select_temperature_sensor"),
+            {
+                0: "Internal",
+                1: "External",
+                2: "Both",
+            },
+        )
+        self.setUpMultiNumber(
+            [
+                {
+                    "name": "number_calibration_offset",
+                    "dps": CALIBRATE_DPS,
+                    "min": -9,
+                    "max": 9,
+                },
+                {
+                    "name": "number_calibration_swing",
+                    "dps": CALIBSWING_DPS,
+                    "min": 1,
+                    "max": 9,
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 3 - 1
tests/devices/test_beca_bhp6000_thermostat.py

@@ -12,7 +12,9 @@ from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT
 
 from ..const import BECA_BHP6000_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, BasicLockTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from .base_device_tests import TuyaDeviceTestCase
 
 LIGHT_DPS = "1"
 TEMPERATURE_DPS = "2"

+ 20 - 3
tests/devices/test_beca_bht002_thermostat.py

@@ -7,11 +7,18 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import (
+    DEVICE_CLASS_TEMPERATURE,
+    STATE_UNAVAILABLE,
+    TEMP_CELSIUS,
+)
 
 from ..const import BECA_BHT002_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, BasicLockTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.sensor import BasicSensorTests
+from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -23,7 +30,9 @@ FLOOR_DPS = "102"
 UNKNOWN104_DPS = "104"
 
 
-class TestBecaBHT002Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCase):
+class TestBecaBHT002Thermostat(
+    BasicLightTests, BasicLockTests, BasicSensorTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
@@ -34,6 +43,14 @@ class TestBecaBHT002Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCa
         self.subject = self.entities.get("climate")
         self.setUpBasicLight(POWER_DPS, self.entities.get("light_display"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicSensor(
+            FLOOR_DPS,
+            self.entities.get("sensor_external_temperature"),
+            unit=TEMP_CELSIUS,
+            device_class=DEVICE_CLASS_TEMPERATURE,
+            state_class="measurement",
+            testdata=(30, 15),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 20 - 3
tests/devices/test_beca_bht6000_thermostat.py

@@ -7,11 +7,18 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import (
+    DEVICE_CLASS_TEMPERATURE,
+    STATE_UNAVAILABLE,
+    TEMP_CELSIUS,
+)
 
 from ..const import BECA_BHT6000_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, BasicLockTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.sensor import BasicSensorTests
+from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -24,7 +31,9 @@ UNKNOWN103_DPS = "103"
 UNKNOWN104_DPS = "104"
 
 
-class TestBecaBHT6000Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestCase):
+class TestBecaBHT6000Thermostat(
+    BasicLightTests, BasicLockTests, BasicSensorTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
@@ -35,6 +44,14 @@ class TestBecaBHT6000Thermostat(BasicLightTests, BasicLockTests, TuyaDeviceTestC
         self.subject = self.entities.get("climate")
         self.setUpBasicLight(POWER_DPS, self.entities.get("light_display"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicSensor(
+            FLOOR_DPS,
+            self.entities.get("sensor_external_temperature"),
+            unit=TEMP_CELSIUS,
+            device_class=DEVICE_CLASS_TEMPERATURE,
+            state_class="measurement",
+            testdata=(36, 18),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 9 - 1
tests/devices/test_bwt_heatpump.py

@@ -1,3 +1,4 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -8,6 +9,7 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import BWT_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -17,12 +19,18 @@ PRESET_DPS = "4"
 ERROR_DPS = "9"
 
 
-class TestBWTHeatpump(TuyaDeviceTestCase):
+class TestBWTHeatpump(BasicBinarySensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("bwt_heatpump.yaml", BWT_HEATPUMP_PAYLOAD)
         self.subject = self.entities["climate"]
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_water_flow"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 3 - 6
tests/devices/test_deta_fan.py

@@ -3,12 +3,9 @@ from homeassistant.components.light import COLOR_MODE_ONOFF
 
 from ..const import DETA_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import (
-    BasicLightTests,
-    BasicSwitchTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-)
+from ..mixins.light import BasicLightTests
+from ..mixins.switch import BasicSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 SPEED_DPS = "3"

+ 48 - 6
tests/devices/test_eanons_humidifier.py

@@ -1,3 +1,4 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     FAN_HIGH,
     FAN_MEDIUM,
@@ -21,7 +22,11 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import EANONS_HUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicSwitchTests, SwitchableTests, TuyaDeviceTestCase
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.select import BasicSelectTests
+from ..mixins.sensor import BasicSensorTests
+from ..mixins.switch import BasicSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 FANMODE_DPS = "2"
 TIMERHR_DPS = "3"
@@ -34,16 +39,53 @@ CURRENTHUMID_DPS = "16"
 SWITCH_DPS = "22"
 
 
-class TestEanonsHumidifier(BasicSwitchTests, SwitchableTests, TuyaDeviceTestCase):
+class TestEanonsHumidifier(
+    BasicBinarySensorTests,
+    BasicSelectTests,
+    BasicSensorTests,
+    BasicSwitchTests,
+    SwitchableTests,
+    TuyaDeviceTestCase,
+):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("eanons_humidifier.yaml", EANONS_HUMIDIFIER_PAYLOAD)
-        self.subject = self.entities["humidifier"]
+        self.subject = self.entities.get("humidifier")
         self.setUpSwitchable(HVACMODE_DPS, self.subject)
-        self.climate = self.entities["climate"]
-        self.fan = self.entities["fan_intensity"]
-        self.setUpBasicSwitch(SWITCH_DPS, self.entities["switch_uv_sterilization"])
+        self.climate = self.entities.get("climate")
+        self.fan = self.entities.get("fan_intensity")
+        self.setUpBasicSwitch(SWITCH_DPS, self.entities.get("switch_uv_sterilization"))
+        self.setUpBasicSelect(
+            TIMERHR_DPS,
+            self.entities.get("select_timer"),
+            {
+                "cancel": "Off",
+                "1": "1 hour",
+                "2": "2 hours",
+                "3": "3 hours",
+                "4": "4 hours",
+                "5": "5 hours",
+                "6": "6 hours",
+                "7": "7 hours",
+                "8": "8 hours",
+                "9": "9 hours",
+                "10": "10 hours",
+                "11": "11 hours",
+                "12": "12 hours",
+            },
+        )
+        self.setUpBasicSensor(
+            TIMER_DPS,
+            self.entities.get("sensor_timer"),
+            unit="min",
+        )
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_tank"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 3 - 1
tests/devices/test_eberg_qubo_q40hd_heatpump.py

@@ -21,6 +21,7 @@ from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT
 
 from ..const import EBERG_QUBO_Q40HD_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.number import BasicNumberTests
 from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
@@ -35,7 +36,7 @@ SWING_DPS = "30"
 HVACACTION_DPS = "101"
 
 
-class TestEbergQuboQ40HDHeatpump(TuyaDeviceTestCase):
+class TestEbergQuboQ40HDHeatpump(BasicNumberTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
@@ -44,6 +45,7 @@ class TestEbergQuboQ40HDHeatpump(TuyaDeviceTestCase):
             EBERG_QUBO_Q40HD_PAYLOAD,
         )
         self.subject = self.entities.get("climate")
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
 
     def test_supported_features(self):
         self.assertEqual(

+ 3 - 2
tests/devices/test_electriq_12wminv_heatpump.py

@@ -9,12 +9,13 @@ from homeassistant.components.climate.const import (
     SUPPORT_SWING_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.light import COLOR_MODE_ONOFF
 from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import ELECTRIQ_12WMINV_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, BasicSwitchTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
 TEMPERATURE_DPS = "2"

+ 29 - 3
tests/devices/test_electriq_cd12_dehumidifier.py

@@ -1,9 +1,17 @@
 from homeassistant.components.humidifier import SUPPORT_MODES
-from homeassistant.components.light import COLOR_MODE_ONOFF
+from homeassistant.const import (
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    PERCENTAGE,
+    TEMP_CELSIUS,
+)
 
 from ..const import ELECTRIQ_CD12PW_DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, SwitchableTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.sensor import MultiSensorTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 MODE_DPS = "2"
@@ -14,7 +22,7 @@ CURRENTTEMP_DPS = "103"
 
 
 class TestElectriqCD20ProDehumidifier(
-    BasicLightTests, SwitchableTests, TuyaDeviceTestCase
+    BasicLightTests, MultiSensorTests, SwitchableTests, TuyaDeviceTestCase
 ):
     __test__ = True
 
@@ -25,6 +33,24 @@ class TestElectriqCD20ProDehumidifier(
         self.subject = self.entities.get("humidifier")
         self.setUpSwitchable(SWITCH_DPS, self.subject)
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_display"))
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_current_temperature",
+                    "dps": CURRENTTEMP_DPS,
+                    "unit": TEMP_CELSIUS,
+                    "device_class": DEVICE_CLASS_TEMPERATURE,
+                    "state_class": "measurement",
+                },
+                {
+                    "name": "sensor_current_humidity",
+                    "dps": CURRENTHUMID_DPS,
+                    "unit": PERCENTAGE,
+                    "device_class": DEVICE_CLASS_HUMIDITY,
+                    "state_class": "measurement",
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(self.subject.supported_features, SUPPORT_MODES)

+ 47 - 10
tests/devices/test_electriq_cd20_dehumidifier.py

@@ -1,15 +1,18 @@
 from homeassistant.components.fan import SUPPORT_PRESET_MODE
 from homeassistant.components.humidifier import SUPPORT_MODES
-from homeassistant.components.light import COLOR_MODE_ONOFF
+from homeassistant.const import (
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    PERCENTAGE,
+    TEMP_CELSIUS,
+)
 
 from ..const import ELECTRIQ_CD20PRO_DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import (
-    BasicLightTests,
-    BasicSwitchTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-)
+from ..mixins.light import BasicLightTests
+from ..mixins.sensor import MultiSensorTests
+from ..mixins.switch import MultiSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 MODE_DPS = "2"
@@ -23,7 +26,11 @@ CURRENTTEMP_DPS = "103"
 
 
 class TestElectriqCD20ProDehumidifier(
-    BasicLightTests, BasicSwitchTests, SwitchableTests, TuyaDeviceTestCase
+    BasicLightTests,
+    MultiSensorTests,
+    MultiSwitchTests,
+    SwitchableTests,
+    TuyaDeviceTestCase,
 ):
     __test__ = True
 
@@ -35,7 +42,30 @@ class TestElectriqCD20ProDehumidifier(
         self.fan = self.entities.get("fan")
         self.setUpSwitchable(SWITCH_DPS, self.subject)
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_display"))
-        self.setUpBasicSwitch(UV_DPS, self.entities.get("switch_uv_sterilization"))
+        self.setUpMultiSwitch(
+            [
+                {"dps": UV_DPS, "name": "switch_uv_sterilization"},
+                {"dps": ANION_DPS, "name": "switch_ionizer"},
+            ]
+        )
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_current_temperature",
+                    "dps": CURRENTTEMP_DPS,
+                    "unit": TEMP_CELSIUS,
+                    "device_class": DEVICE_CLASS_TEMPERATURE,
+                    "state_class": "measurement",
+                },
+                {
+                    "name": "sensor_current_humidity",
+                    "dps": CURRENTHUMID_DPS,
+                    "unit": PERCENTAGE,
+                    "device_class": DEVICE_CLASS_HUMIDITY,
+                    "state_class": "measurement",
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(self.subject.supported_features, SUPPORT_MODES)
@@ -59,7 +89,14 @@ class TestElectriqCD20ProDehumidifier(
         self.dps[MODE_DPS] = "high"
         self.assertEqual(self.subject.icon, "mdi:tshirt-crew-outline")
 
-        self.assertEqual(self.basicSwitch.icon, "mdi:solar-power")
+        self.assertEqual(
+            self.multiSwitch["switch_uv_sterilization"].icon,
+            "mdi:solar-power",
+        )
+        self.assertEqual(
+            self.multiSwitch["switch_ionizer"].icon,
+            "mdi:creation",
+        )
         self.dps[LIGHT_DPS] = True
         self.assertEqual(self.basicLight.icon, "mdi:led-on")
         self.dps[LIGHT_DPS] = False

+ 30 - 8
tests/devices/test_electriq_cd25_dehumidifier.py

@@ -1,16 +1,19 @@
 from homeassistant.components.fan import SUPPORT_PRESET_MODE
 from homeassistant.components.humidifier import SUPPORT_MODES
-from homeassistant.components.light import COLOR_MODE_ONOFF
+from homeassistant.const import (
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    PERCENTAGE,
+    TEMP_CELSIUS,
+)
 
 from ..const import ELECTRIQ_DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import (
-    BasicLightTests,
-    BasicLockTests,
-    BasicSwitchTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-)
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.sensor import MultiSensorTests
+from ..mixins.switch import BasicSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 MODE_DPS = "2"
@@ -27,6 +30,7 @@ class TestElectriqCD25ProDehumidifier(
     BasicLightTests,
     BasicLockTests,
     BasicSwitchTests,
+    MultiSensorTests,
     SwitchableTests,
     TuyaDeviceTestCase,
 ):
@@ -42,6 +46,24 @@ class TestElectriqCD25ProDehumidifier(
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_uv_sterilization"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
         self.setUpBasicSwitch(IONIZER_DPS, self.entities.get("switch_ionizer"))
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_current_temperature",
+                    "dps": CURRENTTEMP_DPS,
+                    "unit": TEMP_CELSIUS,
+                    "device_class": DEVICE_CLASS_TEMPERATURE,
+                    "state_class": "measurement",
+                },
+                {
+                    "name": "sensor_current_humidity",
+                    "dps": CURRENTHUMID_DPS,
+                    "unit": PERCENTAGE,
+                    "device_class": DEVICE_CLASS_HUMIDITY,
+                    "state_class": "measurement",
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(self.subject.supported_features, SUPPORT_MODES)

+ 17 - 3
tests/devices/test_electriq_desd9lw_dehumidifier.py

@@ -9,11 +9,18 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_HUMIDITY,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import (
+    DEVICE_CLASS_HUMIDITY,
+    PERCENTAGE,
+    STATE_UNAVAILABLE,
+)
 
 from ..const import ELECTRIQ_DESD9LW_DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, BasicSwitchTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.sensor import BasicSensorTests
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
 HUMIDITY_DPS = "2"
@@ -28,7 +35,7 @@ TEMPERATURE_DPS = "101"
 
 
 class TestElectriqDESD9LWDehumidifier(
-    BasicLightTests, BasicSwitchTests, TuyaDeviceTestCase
+    BasicLightTests, BasicSensorTests, BasicSwitchTests, TuyaDeviceTestCase
 ):
     __test__ = True
 
@@ -40,6 +47,13 @@ class TestElectriqDESD9LWDehumidifier(
         self.subject = self.entities.get("climate")
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_uv_sterilization"))
         self.setUpBasicSwitch(SWITCH_DPS, self.entities.get("switch_ionizer"))
+        self.setUpBasicSensor(
+            CURRENTHUM_DPS,
+            self.entities.get("sensor_current_humidity"),
+            unit=PERCENTAGE,
+            device_class=DEVICE_CLASS_HUMIDITY,
+            state_class="measurement",
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 9 - 1
tests/devices/test_eurom_600_heater.py

@@ -1,3 +1,4 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -7,6 +8,7 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import EUROM_600_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -15,12 +17,18 @@ CURRENTTEMP_DPS = "5"
 ERROR_DPS = "6"
 
 
-class TestEurom600Heater(TuyaDeviceTestCase):
+class TestEurom600Heater(BasicBinarySensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("eurom_600_heater.yaml", EUROM_600_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_error"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 23 - 3
tests/devices/test_gardenpac_heatpump.py

@@ -1,10 +1,15 @@
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
+    CURRENT_HVAC_HEAT,
+    CURRENT_HVAC_IDLE,
+    CURRENT_HVAC_OFF,
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
 from homeassistant.const import (
+    DEVICE_CLASS_POWER_FACTOR,
+    PERCENTAGE,
     STATE_UNAVAILABLE,
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
@@ -12,6 +17,7 @@ from homeassistant.const import (
 
 from ..const import GARDENPAC_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.sensor import BasicSensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -27,12 +33,19 @@ UNKNOWN116_DPS = "116"
 PRESET_DPS = "117"
 
 
-class TestGardenPACPoolHeatpump(TuyaDeviceTestCase):
+class TestGardenPACPoolHeatpump(BasicSensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("gardenpac_heatpump.yaml", GARDENPAC_HEATPUMP_PAYLOAD)
         self.subject = self.entities.get("climate")
+        self.setUpBasicSensor(
+            POWERLEVEL_DPS,
+            self.entities.get("sensor_power_level"),
+            unit=PERCENTAGE,
+            device_class=DEVICE_CLASS_POWER_FACTOR,
+            state_class="measurement",
+        )
 
     def test_supported_features(self):
         self.assertEqual(
@@ -164,9 +177,17 @@ class TestGardenPACPoolHeatpump(TuyaDeviceTestCase):
         ):
             await self.subject.async_set_preset_mode("Smart")
 
+    def test_hvac_action(self):
+        self.dps[HVACMODE_DPS] = True
+        self.dps[OPMODE_DPS] = "heating"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_HEAT)
+        self.dps[OPMODE_DPS] = "warm"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_IDLE)
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_OFF)
+
     def test_device_state_attributes(self):
         self.dps[POWERLEVEL_DPS] = 50
-        self.dps[OPMODE_DPS] = "cool"
         self.dps[UNKNOWN107_DPS] = 1
         self.dps[UNKNOWN108_DPS] = 2
         self.dps[UNKNOWN115_DPS] = 3
@@ -175,7 +196,6 @@ class TestGardenPACPoolHeatpump(TuyaDeviceTestCase):
             self.subject.device_state_attributes,
             {
                 "power_level": 50,
-                "operating_mode": "cool",
                 "unknown_107": 1,
                 "unknown_108": 2,
                 "unknown_115": 3,

+ 45 - 45
tests/devices/test_goldair_dehumidifier.py

@@ -24,12 +24,12 @@ from homeassistant.const import (
 
 from ..const import DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import (
-    BasicLockTests,
-    BasicSwitchTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-)
+from ..mixins.binary_sensor import MultiBinarySensorTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from ..mixins.sensor import MultiSensorTests
+from ..mixins.switch import BasicSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 PRESET_DPS = "2"
@@ -38,7 +38,7 @@ AIRCLEAN_DPS = "5"
 FANMODE_DPS = "6"
 LOCK_DPS = "7"
 ERROR_DPS = "11"
-UNKNOWN12_DPS = "12"
+TIMER_DPS = "12"
 UNKNOWN101_DPS = "101"
 LIGHTOFF_DPS = "102"
 CURRENTTEMP_DPS = "103"
@@ -55,7 +55,10 @@ ERROR_TANK = "Tank full or missing"
 
 class TestGoldairDehumidifier(
     BasicLockTests,
+    BasicNumberTests,
     BasicSwitchTests,
+    MultiBinarySensorTests,
+    MultiSensorTests,
     SwitchableTests,
     TuyaDeviceTestCase,
 ):
@@ -71,10 +74,41 @@ class TestGoldairDehumidifier(
         self.light = self.entities.get("light_display")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
         self.setUpBasicSwitch(AIRCLEAN_DPS, self.entities.get("switch_air_clean"))
-        self.temperature = self.entities.get("sensor_current_temperature")
-        self.humidity = self.entities.get("sensor_current_humidity")
-        self.tank = self.entities.get("binary_sensor_tank")
-        self.defrost = self.entities.get("binary_sensor_defrost")
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_current_temperature",
+                    "dps": CURRENTTEMP_DPS,
+                    "unit": TEMP_CELSIUS,
+                    "device_class": DEVICE_CLASS_TEMPERATURE,
+                    "state_class": "measurement",
+                },
+                {
+                    "name": "sensor_current_humidity",
+                    "dps": CURRENTHUMID_DPS,
+                    "unit": "%",
+                    "device_class": DEVICE_CLASS_HUMIDITY,
+                    "state_class": "measurement",
+                },
+            ]
+        )
+        self.setUpMultiBinarySensors(
+            [
+                {
+                    "name": "binary_sensor_tank",
+                    "dps": ERROR_DPS,
+                    "device_class": DEVICE_CLASS_PROBLEM,
+                    "testdata": (8, 0),
+                },
+                {
+                    "name": "binary_sensor_defrost",
+                    "dps": DEFROST_DPS,
+                    "device_class": DEVICE_CLASS_COLD,
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(
@@ -144,7 +178,6 @@ class TestGoldairDehumidifier(
     def test_current_humidity(self):
         self.dps[CURRENTHUMID_DPS] = 47
         self.assertEqual(self.climate.current_humidity, 47)
-        self.assertEqual(self.humidity.native_value, 47)
 
     def test_min_target_humidity(self):
         self.assertEqual(self.climate.min_humidity, 30)
@@ -252,10 +285,6 @@ class TestGoldairDehumidifier(
             self.climate.temperature_unit,
             self.climate._device.temperature_unit,
         )
-        self.assertEqual(
-            self.temperature.native_unit_of_measurement,
-            TEMP_CELSIUS,
-        )
 
     def test_minimum_target_temperature(self):
         self.assertIs(self.climate.min_temp, None)
@@ -266,7 +295,6 @@ class TestGoldairDehumidifier(
     def test_current_temperature(self):
         self.dps[CURRENTTEMP_DPS] = 25
         self.assertEqual(self.climate.current_temperature, 25)
-        self.assertEqual(self.temperature.native_value, 25)
 
     def test_climate_hvac_mode(self):
         self.dps[HVACMODE_DPS] = True
@@ -538,7 +566,6 @@ class TestGoldairDehumidifier(
         self.dps[ERROR_DPS] = None
         self.dps[DEFROST_DPS] = False
         self.dps[AIRCLEAN_DPS] = False
-        self.dps[UNKNOWN12_DPS] = "something"
         self.dps[UNKNOWN101_DPS] = False
         self.assertDictEqual(
             self.climate.device_state_attributes,
@@ -546,7 +573,6 @@ class TestGoldairDehumidifier(
                 "error": None,
                 "defrosting": False,
                 "air_clean_on": False,
-                "unknown_12": "something",
                 "unknown_101": False,
             },
         )
@@ -554,7 +580,6 @@ class TestGoldairDehumidifier(
         self.dps[ERROR_DPS] = 8
         self.dps[DEFROST_DPS] = True
         self.dps[AIRCLEAN_DPS] = True
-        self.dps[UNKNOWN12_DPS] = "something else"
         self.dps[UNKNOWN101_DPS] = True
         self.assertDictEqual(
             self.climate.device_state_attributes,
@@ -562,7 +587,6 @@ class TestGoldairDehumidifier(
                 "error": ERROR_TANK,
                 "defrosting": True,
                 "air_clean_on": True,
-                "unknown_12": "something else",
                 "unknown_101": True,
             },
         )
@@ -623,27 +647,3 @@ class TestGoldairDehumidifier(
 
     def test_switch_icon(self):
         self.assertEqual(self.basicSwitch.icon, "mdi:air-purifier")
-
-    def test_sensor_state_class(self):
-        self.assertEqual(self.temperature.state_class, "measurement")
-        self.assertEqual(self.humidity.state_class, "measurement")
-
-    def test_sensor_device_class(self):
-        self.assertEqual(self.temperature.device_class, DEVICE_CLASS_TEMPERATURE)
-        self.assertEqual(self.humidity.device_class, DEVICE_CLASS_HUMIDITY)
-
-    def test_binary_sensor_device_class(self):
-        self.assertEqual(self.tank.device_class, DEVICE_CLASS_PROBLEM)
-        self.assertEqual(self.defrost.device_class, DEVICE_CLASS_COLD)
-
-    def test_binary_sensor_is_on(self):
-        self.dps[ERROR_DPS] = 0
-        self.dps[DEFROST_DPS] = False
-        self.assertFalse(self.tank.is_on)
-        self.assertFalse(self.defrost.is_on)
-        self.dps[ERROR_DPS] = 8
-        self.dps[DEFROST_DPS] = True
-        self.assertTrue(self.tank.is_on)
-        self.assertTrue(self.defrost.is_on)
-        self.dps[ERROR_DPS] = 1
-        self.assertFalse(self.tank.is_on)

+ 3 - 2
tests/devices/test_goldair_fan.py

@@ -14,13 +14,14 @@ from homeassistant.components.fan import (
     SUPPORT_PRESET_MODE,
     SUPPORT_SET_SPEED,
 )
-from homeassistant.components.light import COLOR_MODE_ONOFF
 
 from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, SwitchableTests, TuyaDeviceTestCase
+from ..mixins.light import BasicLightTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 FANMODE_DPS = "2"

+ 15 - 3
tests/devices/test_goldair_geco_heater.py

@@ -1,14 +1,17 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import GECO_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 LOCK_DPS = "2"
@@ -18,13 +21,22 @@ TIMER_DPS = "5"
 ERROR_DPS = "6"
 
 
-class TestGoldairGECOHeater(BasicLockTests, TuyaDeviceTestCase):
+class TestGoldairGECOHeater(
+    BasicBinarySensorTests, BasicLockTests, BasicNumberTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("goldair_geco_heater.yaml", GECO_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_error"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 15 - 3
tests/devices/test_goldair_gpcv_heater.py

@@ -1,15 +1,18 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import GPCV_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 LOCK_DPS = "2"
@@ -20,13 +23,22 @@ ERROR_DPS = "6"
 PRESET_DPS = "7"
 
 
-class TestGoldairGPCVHeater(BasicLockTests, TuyaDeviceTestCase):
+class TestGoldairGPCVHeater(
+    BasicBinarySensorTests, BasicLockTests, BasicNumberTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("goldair_gpcv_heater.yaml", GPCV_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_error"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 32 - 30
tests/devices/test_goldair_gpph_heater.py

@@ -1,5 +1,6 @@
 from unittest import skip
 
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -8,11 +9,16 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_TEMPERATURE,
 )
 
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import DEVICE_CLASS_POWER_FACTOR, PERCENTAGE, STATE_UNAVAILABLE
 
 from ..const import GPPH_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLightTests, BasicLockTests, TuyaDeviceTestCase
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from ..mixins.sensor import BasicSensorTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"
@@ -28,7 +34,14 @@ SWING_DPS = "105"
 ECOTEMP_DPS = "106"
 
 
-class TestGoldairHeater(BasicLightTests, BasicLockTests, TuyaDeviceTestCase):
+class TestGoldairHeater(
+    BasicBinarySensorTests,
+    BasicLightTests,
+    BasicLockTests,
+    BasicNumberTests,
+    BasicSensorTests,
+    TuyaDeviceTestCase,
+):
     __test__ = True
 
     def setUp(self):
@@ -36,7 +49,22 @@ class TestGoldairHeater(BasicLightTests, BasicLockTests, TuyaDeviceTestCase):
         self.subject = self.entities.get("climate")
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_display"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
-        self.timer = self.entities.get("number_timer")
+        self.setUpBasicNumber(
+            TIMER_DPS, self.entities.get("number_timer"), min=0, max=1440, step=60
+        )
+        self.setUpBasicSensor(
+            POWERLEVEL_DPS,
+            self.entities.get("sensor_power_level"),
+            unit=PERCENTAGE,
+            device_class=DEVICE_CLASS_POWER_FACTOR,
+            testdata=("2", 40),
+        )
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_error"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(
@@ -334,29 +362,3 @@ class TestGoldairHeater(BasicLightTests, BasicLockTests, TuyaDeviceTestCase):
 
         self.dps[LIGHT_DPS] = False
         self.assertEqual(self.basicLight.icon, "mdi:led-off")
-
-    def test_timer_min_value(self):
-        self.assertEqual(self.timer.min_value, 0)
-
-    def test_timer_max_value(self):
-        self.assertEqual(self.timer.max_value, 1440)
-
-    def test_timer_step(self):
-        self.assertEqual(self.timer.step, 60)
-
-    def test_timer_mode(self):
-        self.assertEqual(self.timer.mode, "auto")
-
-    def test_timer_value(self):
-        self.dps[TIMER_DPS] = 1234
-        self.assertEqual(self.timer.value, 1234)
-
-    async def test_timer_set_value(self):
-        async with assert_device_properties_set(
-            self.timer._device,
-            {TIMER_DPS: 120},
-        ):
-            await self.timer.async_set_value(120)
-
-    def test_number_device_state_attributes(self):
-        self.assertEqual(self.timer.device_state_attributes, {})

+ 39 - 113
tests/devices/test_grid_connect_double_power_point.py

@@ -2,7 +2,8 @@
 from homeassistant.components.switch import DEVICE_CLASS_OUTLET
 
 from ..const import GRIDCONNECT_2SOCKET_PAYLOAD
-from ..helpers import assert_device_properties_set
+from ..mixins.lock import BasicLockTests
+from ..mixins.switch import MultiSwitchTests
 from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH1_DPS = "1"
@@ -23,7 +24,11 @@ LOCK_DPS = "40"
 MASTER_DPS = "101"
 
 
-class TestGridConnectDoubleSwitch(TuyaDeviceTestCase):
+class TestGridConnectDoubleSwitch(
+    BasicLockTests,
+    MultiSwitchTests,
+    TuyaDeviceTestCase,
+):
     __test__ = True
 
     def setUp(self):
@@ -31,122 +36,43 @@ class TestGridConnectDoubleSwitch(TuyaDeviceTestCase):
             "grid_connect_usb_double_power_point.yaml",
             GRIDCONNECT_2SOCKET_PAYLOAD,
         )
-        self.subject = self.entities.get("switch_master")
-        self.switch1 = self.entities.get("switch_outlet_1")
-        self.switch2 = self.entities.get("switch_outlet_2")
-        self.lock = self.entities.get("lock_child_lock")
-
-    def test_device_class_is_outlet(self):
-        self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)
-
-    def test_is_on(self):
-        self.dps[MASTER_DPS] - True
-        self.assertTrue(self.subject.is_on)
-
-        self.dps[MASTER_DPS] = False
-        self.assertFalse(self.subject.is_on)
-
-        self.assertIsNone(self.switch1.is_on)
-        self.assertIsNone(self.switch1.is_on)
-
-        self.dps[MASTER_DPS] = True
-        self.dps[SWITCH1_DPS] = True
-        self.dps[SWITCH2_DPS] = False
-        self.assertTrue(self.switch1.is_on)
-        self.assertFalse(self.switch2.is_on)
-
-        self.dps[SWITCH1_DPS] = False
-        self.dps[SWITCH2_DPS] = True
-        self.assertFalse(self.switch1.is_on)
-        self.assertTrue(self.switch2.is_on)
-
-    def test_is_on_when_unavailable(self):
-        self.dps[MASTER_DPS] = None
-        self.assertIsNone(self.subject.is_on)
-
-    async def test_turn_on(self):
-        async with assert_device_properties_set(
-            self.subject._device, {MASTER_DPS: True}
-        ):
-            await self.subject.async_turn_on()
-        async with assert_device_properties_set(
-            self.switch1._device, {SWITCH1_DPS: True}
-        ):
-            await self.switch1.async_turn_on()
-        async with assert_device_properties_set(
-            self.switch1._device, {SWITCH2_DPS: True}
-        ):
-            await self.switch2.async_turn_on()
-
-    async def test_turn_off(self):
-        async with assert_device_properties_set(
-            self.subject._device, {MASTER_DPS: False}
-        ):
-            await self.subject.async_turn_off()
-        async with assert_device_properties_set(
-            self.switch1._device, {SWITCH1_DPS: False}
-        ):
-            await self.switch1.async_turn_off()
-        async with assert_device_properties_set(
-            self.switch1._device, {SWITCH2_DPS: False}
-        ):
-            await self.switch2.async_turn_off()
-
-    async def test_toggle_turns_the_switch_on_when_it_was_off(self):
-        self.dps[MASTER_DPS] = False
-        self.dps[SWITCH1_DPS] = False
-        self.dps[SWITCH2_DPS] = False
-
-        async with assert_device_properties_set(
-            self.subject._device, {MASTER_DPS: True}
-        ):
-            await self.subject.async_toggle()
-
-        self.dps[MASTER_DPS] = True
-
-        async with assert_device_properties_set(
-            self.subject._device, {SWITCH1_DPS: True}
-        ):
-            await self.switch1.async_toggle()
-
-        async with assert_device_properties_set(
-            self.subject._device, {SWITCH2_DPS: True}
-        ):
-            await self.switch2.async_toggle()
-
-    async def test_toggle_turns_the_switch_off_when_it_was_on(self):
-        self.dps[MASTER_DPS] = True
-        self.dps[SWITCH1_DPS] = True
-        self.dps[SWITCH2_DPS] = True
-        async with assert_device_properties_set(
-            self.subject._device, {SWITCH1_DPS: False}
-        ):
-            await self.switch1.async_toggle()
-
-        async with assert_device_properties_set(
-            self.subject._device, {SWITCH2_DPS: False}
-        ):
-            await self.switch2.async_toggle()
-
-        async with assert_device_properties_set(
-            self.subject._device, {MASTER_DPS: False}
-        ):
-            await self.subject.async_toggle()
+        self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        # Master switch must go last, otherwise its tests interfere with
+        # the tests for the other switches since it overrides them.
+        # Tests for the specific override behaviour are below.
+        self.setUpMultiSwitch(
+            [
+                {
+                    "name": "switch_outlet_1",
+                    "dps": SWITCH1_DPS,
+                    "device_class": DEVICE_CLASS_OUTLET,
+                },
+                {
+                    "name": "switch_outlet_2",
+                    "dps": SWITCH2_DPS,
+                    "device_class": DEVICE_CLASS_OUTLET,
+                },
+                {
+                    "name": "switch_master",
+                    "dps": MASTER_DPS,
+                    "device_class": DEVICE_CLASS_OUTLET,
+                    "power_dps": POWER_DPS,
+                    "power_scale": 10,
+                },
+            ]
+        )
 
     async def test_turn_on_fails_when_master_is_off(self):
         self.dps[MASTER_DPS] = False
         self.dps[SWITCH1_DPS] = False
         self.dps[SWITCH2_DPS] = False
         with self.assertRaises(AttributeError):
-            await self.switch1.async_turn_on()
+            await self.multiSwitch["switch_outlet_1"].async_turn_on()
         with self.assertRaises(AttributeError):
-            await self.switch2.async_turn_on()
-
-    def test_current_power_w(self):
-        self.dps[POWER_DPS] = 1234
-        self.assertEqual(self.subject.current_power_w, 123.4)
+            await self.multiSwitch["switch_outlet_2"].async_turn_on()
 
-    def test_device_state_attributes_set(self):
+    # Since we have attributes, override the default test which expects none.
+    def test_multi_switch_state_attributes(self):
         self.dps[COUNTDOWN1_DPS] = 9
         self.dps[COUNTDOWN2_DPS] = 10
         self.dps[UNKNOWN17_DPS] = 17
@@ -160,7 +86,7 @@ class TestGridConnectDoubleSwitch(TuyaDeviceTestCase):
         self.dps[UNKNOWN25_DPS] = 25
         self.dps[UNKNOWN38_DPS] = "38"
         self.assertDictEqual(
-            self.subject.device_state_attributes,
+            self.multiSwitch["switch_master"].device_state_attributes,
             {
                 "current_a": 1.234,
                 "voltage_v": 235.0,
@@ -175,13 +101,13 @@ class TestGridConnectDoubleSwitch(TuyaDeviceTestCase):
             },
         )
         self.assertDictEqual(
-            self.switch1.device_state_attributes,
+            self.multiSwitch["switch_outlet_1"].device_state_attributes,
             {
                 "countdown": 9,
             },
         )
         self.assertDictEqual(
-            self.switch2.device_state_attributes,
+            self.multiSwitch["switch_outlet_2"].device_state_attributes,
             {
                 "countdown": 10,
             },

+ 96 - 11
tests/devices/test_inkbird_itc306a_thermostat.py

@@ -1,4 +1,11 @@
+from homeassistant.components.binary_sensor import (
+    DEVICE_CLASS_COLD,
+    DEVICE_CLASS_HEAT,
+    DEVICE_CLASS_PROBLEM,
+)
 from homeassistant.components.climate.const import (
+    CURRENT_HVAC_HEAT,
+    CURRENT_HVAC_IDLE,
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE_RANGE,
 )
@@ -7,6 +14,9 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
 
 from ..const import INKBIRD_THERMOSTAT_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import MultiBinarySensorTests
+from ..mixins.number import MultiNumberTests
+from ..mixins.select import BasicSelectTests
 from .base_device_tests import TuyaDeviceTestCase
 
 ERROR_DPS = "12"
@@ -30,7 +40,12 @@ UNKNOWN119_DPS = "119"
 UNKNOWN120_DPS = "120"
 
 
-class TestInkbirdThermostat(TuyaDeviceTestCase):
+class TestInkbirdThermostat(
+    BasicSelectTests,
+    MultiBinarySensorTests,
+    MultiNumberTests,
+    TuyaDeviceTestCase,
+):
     __test__ = True
 
     def setUp(self):
@@ -38,6 +53,72 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
             "inkbird_itc306a_thermostat.yaml", INKBIRD_THERMOSTAT_PAYLOAD
         )
         self.subject = self.entities.get("climate")
+        self.setUpBasicSelect(
+            UNIT_DPS,
+            self.entities.get("select_temperature_unit"),
+            {
+                "C": "Celsius",
+                "F": "Fahrenheit",
+            },
+        )
+        self.setUpMultiBinarySensors(
+            [
+                {
+                    "name": "binary_sensor_high_temperature",
+                    "dps": ALARM_HIGH_DPS,
+                    "device_class": DEVICE_CLASS_HEAT,
+                },
+                {
+                    "name": "binary_sensor_low_temperature",
+                    "dps": ALARM_LOW_DPS,
+                    "device_class": DEVICE_CLASS_COLD,
+                },
+                {
+                    "name": "binary_sensor_continuous_heat",
+                    "dps": ALARM_TIME_DPS,
+                    "device_class": DEVICE_CLASS_PROBLEM,
+                },
+                {
+                    "name": "binary_sensor_error",
+                    "dps": ERROR_DPS,
+                    "device_class": DEVICE_CLASS_PROBLEM,
+                    "testdata": (1, 0),
+                },
+            ]
+        )
+        self.setUpMultiNumber(
+            [
+                {
+                    "name": "number_calibration_offset",
+                    "dps": CALIBRATE_DPS,
+                    "scale": 10,
+                    "step": 0.1,
+                    "min": -9.9,
+                    "max": 9.9,
+                },
+                {
+                    "name": "number_continuous_heat_hours",
+                    "dps": TIME_THRES_DPS,
+                    "max": 96,
+                },
+                {
+                    "name": "number_high_temperature_limit",
+                    "dps": HIGH_THRES_DPS,
+                    "scale": 10,
+                    "step": 0.1,
+                    "min": -40,
+                    "max": 100,
+                },
+                {
+                    "name": "number_low_temperature_limit",
+                    "dps": LOW_THRES_DPS,
+                    "scale": 10,
+                    "step": 0.1,
+                    "min": -40,
+                    "max": 100,
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(
@@ -135,15 +216,15 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
 
     def test_minimum_target_temperature(self):
         self.dps[UNIT_DPS] = "C"
-        self.assertEqual(self.subject.min_temp, 20.0)
+        self.assertEqual(self.subject.min_temp, 0.0)
         self.dps[UNIT_DPS] = "F"
-        self.assertEqual(self.subject.min_temp, 68.0)
+        self.assertEqual(self.subject.min_temp, 32.0)
 
     def test_maximum_target_temperature(self):
         self.dps[UNIT_DPS] = "C"
-        self.assertEqual(self.subject.max_temp, 35.0)
+        self.assertEqual(self.subject.max_temp, 45.0)
         self.dps[UNIT_DPS] = "F"
-        self.assertEqual(self.subject.max_temp, 95.0)
+        self.assertEqual(self.subject.max_temp, 113.0)
 
     def test_temperature_range(self):
         self.dps[TEMPHIGH_DPS] = 301
@@ -166,20 +247,26 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
     async def test_set_target_temperature_fails_outside_valid_range(self):
         self.dps[UNIT_DPS] = "C"
         with self.assertRaisesRegex(
-            ValueError, "target_temp_low \\(19.9\\) must be between 20.0 and 35.0"
+            ValueError, "target_temp_low \\(-0.1\\) must be between 0.0 and 45.0"
         ):
             await self.subject.async_set_temperature(
-                target_temp_high=32.2, target_temp_low=19.9
+                target_temp_high=32.2, target_temp_low=-0.1
             )
 
         self.dps[UNIT_DPS] = "F"
         with self.assertRaisesRegex(
-            ValueError, "target_temp_high \\(95.1\\) must be between 68.0 and 95.0"
+            ValueError, "target_temp_high \\(113.1\\) must be between 32.0 and 113.0"
         ):
             await self.subject.async_set_temperature(
-                target_temp_low=70.0, target_temp_high=95.1
+                target_temp_low=70.0, target_temp_high=113.1
             )
 
+    def test_hvac_action(self):
+        self.dps[SWITCH_DPS] = False
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_IDLE)
+        self.dps[SWITCH_DPS] = True
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_HEAT)
+
     def test_device_state_attributes(self):
         self.dps[ERROR_DPS] = 1
         self.dps[CALIBRATE_DPS] = 1
@@ -189,7 +276,6 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
         self.dps[ALARM_HIGH_DPS] = True
         self.dps[ALARM_LOW_DPS] = False
         self.dps[ALARM_TIME_DPS] = True
-        self.dps[SWITCH_DPS] = False
         self.dps[TEMPF_DPS] = 999
         self.dps[UNKNOWN117_DPS] = True
         self.dps[UNKNOWN118_DPS] = False
@@ -207,7 +293,6 @@ class TestInkbirdThermostat(TuyaDeviceTestCase):
                 "high_temp_alarm": True,
                 "low_temp_alarm": False,
                 "heat_time_alarm": True,
-                "switch_state": False,
                 "current_temperature_f": 99.9,
                 "unknown_117": True,
                 "unknown_118": False,

+ 25 - 3
tests/devices/test_kogan_dehumidifier.py

@@ -1,9 +1,16 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED
 from homeassistant.components.humidifier import SUPPORT_MODES
-
+from homeassistant.const import (
+    DEVICE_CLASS_HUMIDITY,
+    PERCENTAGE,
+)
 from ..const import KOGAN_DEHUMIDIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import SwitchableTests, TuyaDeviceTestCase
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.sensor import BasicSensorTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 MODE_DPS = "2"
@@ -15,7 +22,9 @@ COUNTDOWN_DPS = "13"
 HUMIDITY_DPS = "101"
 
 
-class TestKoganDehumidifier(SwitchableTests, TuyaDeviceTestCase):
+class TestKoganDehumidifier(
+    BasicBinarySensorTests, BasicSensorTests, SwitchableTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
@@ -23,6 +32,19 @@ class TestKoganDehumidifier(SwitchableTests, TuyaDeviceTestCase):
         self.subject = self.entities.get("humidifier")
         self.setUpSwitchable(SWITCH_DPS, self.subject)
         self.fan = self.entities.get("fan")
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_tank"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
+        self.setUpBasicSensor(
+            CURRENTHUMID_DPS,
+            self.entities.get("sensor_current_humidity"),
+            device_class=DEVICE_CLASS_HUMIDITY,
+            state_class="measurement",
+            unit=PERCENTAGE,
+        )
 
     def test_supported_features(self):
         self.assertEqual(self.subject.supported_features, SUPPORT_MODES)

+ 5 - 3
tests/devices/test_kogan_kahtp_heater.py

@@ -4,12 +4,13 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import KOGAN_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from .base_device_tests import TuyaDeviceTestCase
 
 TEMPERATURE_DPS = "2"
 CURRENTTEMP_DPS = "3"
@@ -19,13 +20,14 @@ HVACMODE_DPS = "7"
 TIMER_DPS = "8"
 
 
-class TestGoldairKoganKAHTPHeater(BasicLockTests, TuyaDeviceTestCase):
+class TestGoldairKoganKAHTPHeater(BasicLockTests, BasicNumberTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("kogan_kahtp_heater.yaml", KOGAN_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
 
     def test_supported_features(self):
         self.assertEqual(

+ 5 - 3
tests/devices/test_kogan_kawfhtp_heater.py

@@ -4,12 +4,13 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
 from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import KOGAN_KAWFHTP_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "3"
@@ -19,13 +20,14 @@ PRESET_DPS = "7"
 LOCK_DPS = "2"
 
 
-class TestGoldairKoganKAHTPHeater(BasicLockTests, TuyaDeviceTestCase):
+class TestGoldairKoganKAHTPHeater(BasicLockTests, BasicNumberTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("kogan_kawfhtp_heater.yaml", KOGAN_KAWFHTP_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
 
     def test_supported_features(self):
         self.assertEqual(

+ 7 - 7
tests/devices/test_lexy_f501_fan.py

@@ -6,13 +6,11 @@ from homeassistant.components.fan import (
 
 from ..const import LEXY_F501_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import (
-    BasicLightTests,
-    BasicLockTests,
-    BasicSwitchTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-)
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import BasicNumberTests
+from ..mixins.switch import BasicSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
 PRESET_DPS = "2"
@@ -28,6 +26,7 @@ class TestLexyF501Fan(
     SwitchableTests,
     BasicLightTests,
     BasicLockTests,
+    BasicNumberTests,
     BasicSwitchTests,
     TuyaDeviceTestCase,
 ):
@@ -39,6 +38,7 @@ class TestLexyF501Fan(
         self.setUpSwitchable(POWER_DPS, self.subject)
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=7)
         self.setUpBasicSwitch(SWITCH_DPS, self.entities.get("switch_sound"))
 
     def test_supported_features(self):

+ 22 - 3
tests/devices/test_madimack_heatpump.py

@@ -1,10 +1,15 @@
 from homeassistant.components.climate.const import (
+    CURRENT_HVAC_HEAT,
+    CURRENT_HVAC_IDLE,
+    CURRENT_HVAC_OFF,
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
 from homeassistant.const import (
+    DEVICE_CLASS_POWER_FACTOR,
+    PERCENTAGE,
     STATE_UNAVAILABLE,
     TEMP_CELSIUS,
     TEMP_FAHRENHEIT,
@@ -12,6 +17,7 @@ from homeassistant.const import (
 
 from ..const import MADIMACK_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.sensor import BasicSensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -42,12 +48,18 @@ UNKNOWN140_DPS = "140"
 PRESET_DPS = "117"
 
 
-class TestMadimackPoolHeatpump(TuyaDeviceTestCase):
+class TestMadimackPoolHeatpump(BasicSensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("madimack_heatpump.yaml", MADIMACK_HEATPUMP_PAYLOAD)
         self.subject = self.entities.get("climate")
+        self.setUpBasicSensor(
+            POWERLEVEL_DPS,
+            self.entities.get("sensor_power_level"),
+            device_class=DEVICE_CLASS_POWER_FACTOR,
+            unit=PERCENTAGE,
+        )
 
     def test_supported_features(self):
         self.assertEqual(
@@ -179,9 +191,17 @@ class TestMadimackPoolHeatpump(TuyaDeviceTestCase):
         ):
             await self.subject.async_set_preset_mode("Boost")
 
+    def test_hvac_action(self):
+        self.dps[HVACMODE_DPS] = True
+        self.dps[OPMODE_DPS] = "heating"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_HEAT)
+        self.dps[OPMODE_DPS] = "warm"
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_IDLE)
+        self.dps[HVACMODE_DPS] = False
+        self.assertEqual(self.subject.hvac_action, CURRENT_HVAC_OFF)
+
     def test_device_state_attributes(self):
         self.dps[POWERLEVEL_DPS] = 50
-        self.dps[OPMODE_DPS] = "cool"
         self.dps[UNKNOWN107_DPS] = 1
         self.dps[UNKNOWN108_DPS] = 2
         self.dps[UNKNOWN115_DPS] = 3
@@ -205,7 +225,6 @@ class TestMadimackPoolHeatpump(TuyaDeviceTestCase):
             self.subject.device_state_attributes,
             {
                 "power_level": 50,
-                "operating_mode": "cool",
                 "unknown_107": 1,
                 "unknown_108": 2,
                 "unknown_115": 3,

+ 2 - 1
tests/devices/test_moes_bht002_thermostat.py

@@ -11,7 +11,8 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import MOES_BHT002_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.lock import BasicLockTests
+from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
 TEMPERATURE_DPS = "2"

+ 9 - 1
tests/devices/test_poolex_silverline_heatpump.py

@@ -1,3 +1,4 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -8,6 +9,7 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import POOLEX_SILVERLINE_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -17,7 +19,7 @@ PRESET_DPS = "4"
 ERROR_DPS = "13"
 
 
-class TestPoolexSilverlineHeatpump(TuyaDeviceTestCase):
+class TestPoolexSilverlineHeatpump(BasicBinarySensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
@@ -25,6 +27,12 @@ class TestPoolexSilverlineHeatpump(TuyaDeviceTestCase):
             "poolex_silverline_heatpump.yaml", POOLEX_SILVERLINE_HEATPUMP_PAYLOAD
         )
         self.subject = self.entities.get("climate")
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_water_flow"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(256, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 9 - 1
tests/devices/test_poolex_vertigo_heatpump.py

@@ -1,3 +1,4 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -8,6 +9,7 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import POOLEX_VERTIGO_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -17,7 +19,7 @@ PRESET_DPS = "4"
 ERROR_DPS = "9"
 
 
-class TestPoolexVertigoHeatpump(TuyaDeviceTestCase):
+class TestPoolexVertigoHeatpump(BasicBinarySensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
@@ -25,6 +27,12 @@ class TestPoolexVertigoHeatpump(TuyaDeviceTestCase):
             "poolex_vertigo_heatpump.yaml", POOLEX_VERTIGO_HEATPUMP_PAYLOAD
         )
         self.subject = self.entities.get("climate")
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_water_flow"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(4, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 2 - 1
tests/devices/test_purline_m100_heater.py

@@ -17,7 +17,8 @@ from ..helpers import (
     assert_device_properties_set,
     assert_device_properties_set_optional,
 )
-from .base_device_tests import BasicSwitchTests, TuyaDeviceTestCase
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
 TEMPERATURE_DPS = "2"

+ 9 - 1
tests/devices/test_remora_heatpump.py

@@ -1,3 +1,4 @@
+from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
 from homeassistant.components.climate.const import (
     HVAC_MODE_HEAT,
     HVAC_MODE_OFF,
@@ -8,6 +9,7 @@ from homeassistant.const import STATE_UNAVAILABLE
 
 from ..const import REMORA_HEATPUMP_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -17,12 +19,18 @@ PRESET_DPS = "4"
 ERROR_DPS = "9"
 
 
-class TestRemoraHeatpump(TuyaDeviceTestCase):
+class TestRemoraHeatpump(BasicBinarySensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("remora_heatpump.yaml", REMORA_HEATPUMP_PAYLOAD)
         self.subject = self.entities.get("climate")
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_water_flow"),
+            device_class=DEVICE_CLASS_PROBLEM,
+            testdata=(1, 0),
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 32 - 10
tests/devices/test_renpho_rp_ap001s.py

@@ -1,17 +1,13 @@
 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 homeassistant.const import DEVICE_CLASS_AQI
 
 from ..const import RENPHO_PURIFIER_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import (
-    BasicLightTests,
-    BasicLockTests,
-    BasicSwitchTests,
-    SwitchableTests,
-    TuyaDeviceTestCase,
-)
+from ..mixins.light import BasicLightTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.sensor import MultiSensorTests
+from ..mixins.switch import BasicSwitchTests, SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 PRESET_DPS = "4"
@@ -30,6 +26,7 @@ class TestRenphoPurifier(
     BasicLightTests,
     BasicLockTests,
     BasicSwitchTests,
+    MultiSensorTests,
     SwitchableTests,
     TuyaDeviceTestCase,
 ):
@@ -42,6 +39,31 @@ class TestRenphoPurifier(
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_aq_indicator"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
         self.setUpBasicSwitch(SLEEP_DPS, self.entities.get("switch_sleep"))
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_air_quality",
+                    "dps": QUALITY_DPS,
+                    "device_class": DEVICE_CLASS_AQI,
+                },
+                {
+                    "name": "sensor_prefilter_life",
+                    "dps": PREFILTER_DPS,
+                },
+                {
+                    "name": "sensor_charcoal_filter_life",
+                    "dps": CHARCOAL_DPS,
+                },
+                {
+                    "name": "sensor_active_filter_life",
+                    "dps": ACTIVATED_DPS,
+                },
+                {
+                    "name": "sensor_hepa_filter_life",
+                    "dps": HEPA_DPS,
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(self.subject.supported_features, SUPPORT_PRESET_MODE)

+ 61 - 4
tests/devices/test_saswell_c16_thermostat.py

@@ -9,12 +9,16 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
 
 from ..const import SASWELL_C16_THERMOSTAT_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import BasicLockTests, TuyaDeviceTestCase
+from ..mixins.lock import BasicLockTests
+from ..mixins.number import MultiNumberTests
+from ..mixins.select import MultiSelectTests
+from ..mixins.sensor import BasicSensorTests
+from ..mixins.switch import BasicSwitchTests
+from .base_device_tests import TuyaDeviceTestCase
 
 TEMPERATURE_DPS = "2"
 PRESET_DPS = "3"
@@ -37,7 +41,14 @@ HVACACTION_DPS = "24"
 UNKNOWN26_DPS = "26"
 
 
-class TestSaswellC16Thermostat(BasicLockTests, TuyaDeviceTestCase):
+class TestSaswellC16Thermostat(
+    BasicLockTests,
+    BasicSensorTests,
+    BasicSwitchTests,
+    MultiNumberTests,
+    MultiSelectTests,
+    TuyaDeviceTestCase,
+):
     __test__ = True
 
     def setUp(self):
@@ -46,6 +57,52 @@ class TestSaswellC16Thermostat(BasicLockTests, TuyaDeviceTestCase):
         )
         self.subject = self.entities.get("climate")
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
+        self.setUpBasicSensor(
+            FLOORTEMP_DPS,
+            self.entities.get("sensor_floor_temperature"),
+            device_class=DEVICE_CLASS_TEMPERATURE,
+            state_class="measurement",
+            unit=TEMP_CELSIUS,
+            testdata=(218, 21.8),
+        )
+        self.setUpBasicSwitch(ADAPTIVE_DPS, self.entities.get("switch_adaptive"))
+        self.setUpMultiNumber(
+            [
+                {
+                    "name": "number_floor_temperature_limit",
+                    "dps": FLOORTEMPLIMIT_DPS,
+                    "min": 20.0,
+                    "max": 50.0,
+                    "scale": 10,
+                    "step": 0.5,
+                },
+                {
+                    "name": "number_power_rating",
+                    "dps": POWERRATING_DPS,
+                    "max": 3500,
+                },
+            ]
+        )
+        self.setUpMultiSelect(
+            [
+                {
+                    "name": "select_installation",
+                    "dps": INSTALL_DPS,
+                    "options": {
+                        True: "Office",
+                        False: "Home",
+                    },
+                },
+                {
+                    "name": "select_schedule",
+                    "dps": SCHED_DPS,
+                    "options": {
+                        "5_1_1": "Weekdays+Sat+Sun",
+                        "7": "Daily",
+                    },
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 24 - 2
tests/devices/test_saswell_t29utk_thermostat.py

@@ -1,7 +1,6 @@
 from homeassistant.components.climate.const import (
     CURRENT_HVAC_COOL,
     CURRENT_HVAC_HEAT,
-    CURRENT_HVAC_IDLE,
     CURRENT_HVAC_OFF,
     FAN_AUTO,
     FAN_ON,
@@ -18,6 +17,7 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
 
 from ..const import SASWELL_T29UTK_THERMOSTAT_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.select import MultiSelectTests
 from .base_device_tests import TuyaDeviceTestCase
 
 POWER_DPS = "1"
@@ -37,7 +37,7 @@ TEMPF_DPS = "116"
 CURTEMPF_DPS = "117"
 
 
-class TestSaswellT29UTKThermostat(TuyaDeviceTestCase):
+class TestSaswellT29UTKThermostat(MultiSelectTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
@@ -45,6 +45,28 @@ class TestSaswellT29UTKThermostat(TuyaDeviceTestCase):
             "saswell_t29utk_thermostat.yaml", SASWELL_T29UTK_THERMOSTAT_PAYLOAD
         )
         self.subject = self.entities.get("climate")
+        self.setUpMultiSelect(
+            [
+                {
+                    "name": "select_temperature_unit",
+                    "dps": UNITS_DPS,
+                    "options": {
+                        "C": "Celsius",
+                        "F": "Fahrenheit",
+                    },
+                },
+                {
+                    "name": "select_configuration",
+                    "dps": CONFIG_DPS,
+                    "options": {
+                        "1": "cooling",
+                        "2": "heating",
+                        "3": "heat/cool",
+                        "5": "heatpump",
+                    },
+                },
+            ]
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 28 - 3
tests/devices/test_smartplugv1.py

@@ -1,9 +1,12 @@
 """Tests for the switch entity."""
 from homeassistant.components.switch import DEVICE_CLASS_OUTLET
+from homeassistant.const import DEVICE_CLASS_CURRENT, DEVICE_CLASS_VOLTAGE
 
 from ..const import KOGAN_SOCKET_PAYLOAD
-from ..helpers import assert_device_properties_set
-from .base_device_tests import SwitchableTests, TuyaDeviceTestCase
+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"
@@ -12,13 +15,35 @@ POWER_DPS = "5"
 VOLTAGE_DPS = "6"
 
 
-class TestKoganSwitch(SwitchableTests, TuyaDeviceTestCase):
+class TestKoganSwitch(
+    BasicNumberTests, MultiSensorTests, SwitchableTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("smartplugv1.yaml", KOGAN_SOCKET_PAYLOAD)
         self.subject = self.entities.get("switch")
         self.setUpSwitchable(SWITCH_DPS, self.subject)
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=1440)
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_voltage",
+                    "dps": VOLTAGE_DPS,
+                    "unit": "V",
+                    "device_class": DEVICE_CLASS_VOLTAGE,
+                    "state_class": "measurement",
+                    "testdata": (2300, 230.0),
+                },
+                {
+                    "name": "sensor_current",
+                    "dps": CURRENT_DPS,
+                    "unit": "mA",
+                    "device_class": DEVICE_CLASS_CURRENT,
+                    "state_class": "measurement",
+                },
+            ]
+        )
 
     def test_device_class_is_outlet(self):
         self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)

+ 28 - 3
tests/devices/test_smartplugv2.py

@@ -1,9 +1,12 @@
 """Tests for the switch entity."""
 from homeassistant.components.switch import DEVICE_CLASS_OUTLET
+from homeassistant.const import DEVICE_CLASS_CURRENT, DEVICE_CLASS_VOLTAGE
 
 from ..const import KOGAN_SOCKET_PAYLOAD2
-from ..helpers import assert_device_properties_set
-from .base_device_tests import SwitchableTests, TuyaDeviceTestCase
+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 = "9"
@@ -12,13 +15,35 @@ POWER_DPS = "19"
 VOLTAGE_DPS = "20"
 
 
-class TestSwitchV2(SwitchableTests, TuyaDeviceTestCase):
+class TestSwitchV2(
+    BasicNumberTests, MultiSensorTests, SwitchableTests, TuyaDeviceTestCase
+):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("smartplugv2.yaml", KOGAN_SOCKET_PAYLOAD2)
         self.subject = self.entities.get("switch")
         self.setUpSwitchable(SWITCH_DPS, self.subject)
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=1440)
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_voltage",
+                    "dps": VOLTAGE_DPS,
+                    "unit": "V",
+                    "device_class": DEVICE_CLASS_VOLTAGE,
+                    "state_class": "measurement",
+                    "testdata": (2300, 230.0),
+                },
+                {
+                    "name": "sensor_current",
+                    "dps": CURRENT_DPS,
+                    "unit": "mA",
+                    "device_class": DEVICE_CLASS_CURRENT,
+                    "state_class": "measurement",
+                },
+            ]
+        )
 
     def test_device_class_is_outlet(self):
         self.assertEqual(self.subject.device_class, DEVICE_CLASS_OUTLET)

+ 2 - 1
tests/devices/test_stirling_fs140dc_fan.py

@@ -6,7 +6,8 @@ from homeassistant.components.fan import (
 
 from ..const import STIRLING_FS1_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
-from .base_device_tests import SwitchableTests, TuyaDeviceTestCase
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
 
 SWITCH_DPS = "1"
 PRESET_DPS = "2"

+ 38 - 2
tests/devices/test_wetair_wch750_heater.py

@@ -8,10 +8,12 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_TEMPERATURE,
 )
 from homeassistant.components.light import COLOR_MODE_BRIGHTNESS
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TIME_MINUTES
 
 from ..const import WETAIR_WCH750_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
+from ..mixins.select import BasicSelectTests
+from ..mixins.sensor import BasicSensorTests
 from .base_device_tests import TuyaDeviceTestCase
 
 HVACMODE_DPS = "1"
@@ -24,13 +26,47 @@ UNKNOWN21_DPS = "21"
 BRIGHTNESS_DPS = "101"
 
 
-class TestWetairWCH750Heater(TuyaDeviceTestCase):
+class TestWetairWCH750Heater(BasicSelectTests, BasicSensorTests, TuyaDeviceTestCase):
     __test__ = True
 
     def setUp(self):
         self.setUpForConfig("wetair_wch750_heater.yaml", WETAIR_WCH750_HEATER_PAYLOAD)
         self.subject = self.entities.get("climate")
         self.light = self.entities.get("light_display")
+        self.setUpBasicSelect(
+            TIMER_DPS,
+            self.entities.get("select_timer"),
+            {
+                "0h": "Off",
+                "1h": "1 hour",
+                "2h": "2 hours",
+                "3h": "3 hours",
+                "4h": "4 hours",
+                "5h": "5 hours",
+                "6h": "6 hours",
+                "7h": "7 hours",
+                "8h": "8 hours",
+                "9h": "9 hours",
+                "10h": "10 hours",
+                "11h": "11 hours",
+                "12h": "12 hours",
+                "13h": "13 hours",
+                "14h": "14 hours",
+                "15h": "15 hours",
+                "16h": "16 hours",
+                "17h": "17 hours",
+                "18h": "18 hours",
+                "19h": "19 hours",
+                "20h": "20 hours",
+                "21h": "21 hours",
+                "22h": "22 hours",
+                "23h": "23 hours",
+                "24h": "24 hours",
+            },
+        )
+        self.setUpBasicSensor(
+            COUNTDOWN_DPS, self.entities.get("sensor_timer"), unit=TIME_MINUTES
+        )
 
     def test_supported_features(self):
         self.assertEqual(

+ 65 - 0
tests/mixins/binary_sensor.py

@@ -0,0 +1,65 @@
+# Mixins for testing sensor entities
+
+
+class BasicBinarySensorTests:
+    def setUpBasicBinarySensor(
+        self,
+        dps,
+        subject,
+        device_class=None,
+        testdata=(True, False),
+    ):
+        self.basicBSensor = subject
+        self.basicBSensorDps = dps
+        self.basicBSensorDeviceClass = device_class
+        self.basicBSensorTestData = testdata
+
+    def test_basic_bsensor_device_class(self):
+        self.assertEqual(self.basicBSensor.device_class, self.basicBSensorDeviceClass)
+
+    def test_basic_bsensor_is_on(self):
+        onval, offval = self.basicBSensorTestData
+        self.dps[self.basicBSensorDps] = onval
+        self.assertTrue(self.basicBSensor.is_on)
+        self.dps[self.basicBSensorDps] = offval
+        self.assertFalse(self.basicBSensor.is_on)
+
+    def test_basic_bsensor_device_state_attributes(self):
+        self.assertEqual(self.basicBSensor.device_state_attributes, {})
+
+
+class MultiBinarySensorTests:
+    def setUpMultiBinarySensors(self, sensors):
+        self.multiBSensor = {}
+        self.multiBSensorDps = {}
+        self.multiBSensorDevClass = {}
+        self.multiBSensorTestData = {}
+        for s in sensors:
+            name = s.get("name")
+            subject = self.entities.get(name)
+            if subject is None:
+                raise AttributeError(f"No binary sensor for {name} found.")
+            self.multiBSensor[name] = subject
+            self.multiBSensorDps[name] = s.get("dps")
+            self.multiBSensorDevClass[name] = s.get("device_class")
+            self.multiBSensorTestData[name] = s.get("testdata", (True, False))
+
+    def test_multi_bsensor_device_class(self):
+        for key, subject in self.multiBSensor.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_class, self.multiBSensorDevClass[key])
+
+    def test_multi_bsensor_is_on(self):
+        for key, subject in self.multiBSensor.items():
+            with self.subTest(key):
+                dps = self.multiBSensorDps[key]
+                onval, offval = self.multiBSensorTestData[key]
+                self.dps[dps] = onval
+                self.assertTrue(subject.is_on)
+                self.dps[dps] = offval
+                self.assertFalse(subject.is_on)
+
+    def test_multi_bsensor_device_state_attributes(self):
+        for key, subject in self.multiBSensor.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_state_attributes, {})

+ 66 - 0
tests/mixins/light.py

@@ -0,0 +1,66 @@
+# Mixins for testing lights
+from homeassistant.components.light import COLOR_MODE_ONOFF
+
+from ..helpers import assert_device_properties_set
+
+
+class BasicLightTests:
+    def setUpBasicLight(self, dps, subject):
+        self.basicLight = subject
+        self.basicLightDps = dps
+
+    def test_basic_light_supported_features(self):
+        self.assertEqual(self.basicLight.supported_features, 0)
+
+    def test_basic_light_supported_color_modes(self):
+        self.assertCountEqual(
+            self.basicLight.supported_color_modes,
+            [COLOR_MODE_ONOFF],
+        )
+
+    def test_basic_light_color_mode(self):
+        self.assertEqual(self.basicLight.color_mode, COLOR_MODE_ONOFF)
+
+    def test_light_has_no_brightness(self):
+        self.assertIsNone(self.basicLight.brightness)
+
+    def test_light_has_no_effects(self):
+        self.assertIsNone(self.basicLight.effect_list)
+        self.assertIsNone(self.basicLight.effect)
+
+    def test_basic_light_is_on(self):
+        self.dps[self.basicLightDps] = True
+        self.assertTrue(self.basicLight.is_on)
+        self.dps[self.basicLightDps] = False
+        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}
+        ):
+            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}
+        ):
+            await self.basicLight.async_turn_off()
+
+    async def test_basic_light_toggle_turns_on_when_it_was_off(self):
+        self.dps[self.basicLightDps] = False
+        async with assert_device_properties_set(
+            self.basicLight._device,
+            {self.basicLightDps: True},
+        ):
+            await self.basicLight.async_toggle()
+
+    async def test_basic_light_toggle_turns_off_when_it_was_on(self):
+        self.dps[self.basicLightDps] = True
+        async with assert_device_properties_set(
+            self.basicLight._device,
+            {self.basicLightDps: False},
+        ):
+            await self.basicLight.async_toggle()
+
+    def test_basic_light_state_attributes(self):
+        self.assertEqual(self.basicLight.device_state_attributes, {})

+ 48 - 0
tests/mixins/lock.py

@@ -0,0 +1,48 @@
+# Mixins for testing locks
+from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED
+from homeassistant.const import STATE_UNAVAILABLE
+
+from ..helpers import assert_device_properties_set
+
+
+class BasicLockTests:
+    def setUpBasicLock(self, dps, subject):
+        self.basicLock = subject
+        self.basicLockDps = dps
+
+    def test_basic_lock_state(self):
+        self.dps[self.basicLockDps] = True
+        self.assertEqual(self.basicLock.state, STATE_LOCKED)
+
+        self.dps[self.basicLockDps] = False
+        self.assertEqual(self.basicLock.state, STATE_UNLOCKED)
+
+        self.dps[self.basicLockDps] = None
+        self.assertEqual(self.basicLock.state, STATE_UNAVAILABLE)
+
+    def test_basic_lock_is_locked(self):
+        self.dps[self.basicLockDps] = True
+        self.assertTrue(self.basicLock.is_locked)
+
+        self.dps[self.basicLockDps] = False
+        self.assertFalse(self.basicLock.is_locked)
+
+        self.dps[self.basicLockDps] = None
+        self.assertFalse(self.basicLock.is_locked)
+
+    async def test_basic_lock_locks(self):
+        async with assert_device_properties_set(
+            self.basicLock._device,
+            {self.basicLockDps: True},
+        ):
+            await self.basicLock.async_lock()
+
+    async def test_basic_lock_unlocks(self):
+        async with assert_device_properties_set(
+            self.basicLock._device,
+            {self.basicLockDps: False},
+        ):
+            await self.basicLock.async_unlock()
+
+    def test_basic_lock_state_attributes(self):
+        self.assertEqual(self.basicLock.device_state_attributes, {})

+ 115 - 0
tests/mixins/number.py

@@ -0,0 +1,115 @@
+# Mixins for testing number entities
+from ..helpers import assert_device_properties_set
+
+
+class BasicNumberTests:
+    def setUpBasicNumber(self, dps, subject, max, min=0, step=1, mode="auto", scale=1):
+        self.basicNumber = subject
+        self.basicNumberDps = dps
+        self.basicNumberMin = min
+        self.basicNumberMax = max
+        self.basicNumberStep = step
+        self.basicNumberMode = mode
+        self.basicNumberScale = scale
+
+    def test_number_min_value(self):
+        self.assertEqual(self.basicNumber.min_value, self.basicNumberMin)
+
+    def test_number_max_value(self):
+        self.assertEqual(self.basicNumber.max_value, self.basicNumberMax)
+
+    def test_number_step(self):
+        self.assertEqual(self.basicNumber.step, self.basicNumberStep)
+
+    def test_number_mode(self):
+        self.assertEqual(self.basicNumber.mode, self.basicNumberMode)
+
+    def test_number_value(self):
+        val = min(max(self.basicNumberMin, self.basicNumberStep), self.basicNumberMax)
+        dps_val = val * self.basicNumberScale
+        self.dps[self.basicNumberDps] = dps_val
+        self.assertEqual(self.basicNumber.value, val)
+
+    async def test_number_set_value(self):
+        val = min(max(self.basicNumberMin, self.basicNumberStep), self.basicNumberMax)
+        dps_val = val * self.basicNumberScale
+        async with assert_device_properties_set(
+            self.basicNumber._device, {self.basicNumberDps: dps_val}
+        ):
+            await self.basicNumber.async_set_value(val)
+
+    def test_number_device_state_attributes(self):
+        self.assertEqual(self.basicNumber.device_state_attributes, {})
+
+
+class MultiNumberTests:
+    def setUpMultiNumber(self, numbers):
+        self.multiNumber = {}
+        self.multiNumberDps = {}
+        self.multiNumberMin = {}
+        self.multiNumberMax = {}
+        self.multiNumberStep = {}
+        self.multiNumberMode = {}
+        self.multiNumberScale = {}
+
+        for n in numbers:
+            name = n.get("name")
+            subject = self.entities.get(name)
+            if subject is None:
+                raise AttributeError(f"No number for {name} found.")
+            self.multiNumber[name] = subject
+            self.multiNumberDps[name] = n.get("dps")
+            self.multiNumberMin[name] = n.get("min", 0)
+            self.multiNumberMax[name] = n.get("max")
+            self.multiNumberStep[name] = n.get("step", 1)
+            self.multiNumberMode[name] = n.get("mode", "auto")
+            self.multiNumberScale[name] = n.get("scale", 1)
+
+    def test_multi_number_min_value(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                self.assertEqual(subject.min_value, self.multiNumberMin[key])
+
+    def test_multi_number_max_value(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                self.assertEqual(subject.max_value, self.multiNumberMax[key])
+
+    def test_multi_number_step(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                self.assertEqual(subject.step, self.multiNumberStep[key])
+
+    def test_multi_number_mode(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                self.assertEqual(subject.mode, self.multiNumberMode[key])
+
+    def test_multi_number_value(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                val = min(
+                    max(self.multiNumberMin[key], self.multiNumberStep[key]),
+                    self.multiNumberMax[key],
+                )
+                dps_val = val * self.multiNumberScale[key]
+                self.dps[self.multiNumberDps[key]] = dps_val
+                self.assertEqual(subject.value, val)
+
+    async def test_multi_number_set_value(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                val = min(
+                    max(self.multiNumberMin[key], self.multiNumberStep[key]),
+                    self.multiNumberMax[key],
+                )
+                dps_val = val * self.multiNumberScale[key]
+                async with assert_device_properties_set(
+                    subject._device, {self.multiNumberDps[key]: dps_val}
+                ):
+                    await subject.async_set_value(val)
+
+    def test_multi_number_device_state_attributes(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_state_attributes, {})

+ 74 - 0
tests/mixins/select.py

@@ -0,0 +1,74 @@
+# Mixins for testing select entities
+from ..helpers import assert_device_properties_set
+
+
+class BasicSelectTests:
+    def setUpBasicSelect(self, dps, subject, options):
+        self.basicSelect = subject
+        self.basicSelectDps = dps
+        self.basicSelectOptions = options
+
+    def test_basicSelect_options(self):
+        self.assertCountEqual(
+            self.basicSelect.options,
+            self.basicSelectOptions.values(),
+        )
+
+    def test_basicSelect_current_option(self):
+        for dpsVal, val in self.basicSelectOptions.items():
+            self.dps[self.basicSelectDps] = dpsVal
+            self.assertEqual(self.basicSelect.current_option, val)
+
+    async def test_basicSelect_select_option(self):
+        for dpsVal, val in self.basicSelectOptions.items():
+            async with assert_device_properties_set(
+                self.basicSelect._device, {self.basicSelectDps: dpsVal}
+            ):
+                await self.basicSelect.async_select_option(val)
+
+    def test_basicSelect_device_state_attributes(self):
+        self.assertEqual(self.basicSelect.device_state_attributes, {})
+
+
+class MultiSelectTests:
+    def setUpMultiSelect(self, selects):
+        self.multiSelect = {}
+        self.multiSelectDps = {}
+        self.multiSelectOptions = {}
+        for s in selects:
+            name = s.get("name")
+            subject = self.entities.get(name)
+            if subject is None:
+                raise AttributeError(f"No select for {name} found.")
+            self.multiSelect[name] = subject
+            self.multiSelectDps[name] = s.get("dps")
+            self.multiSelectOptions[name] = s.get("options")
+
+    def test_multiSelect_options(self):
+        for key, subject in self.multiSelect.items():
+            with self.subTest(key):
+                self.assertCountEqual(
+                    subject.options,
+                    self.multiSelectOptions[key].values(),
+                )
+
+    def test_multiSelect_current_option(self):
+        for key, subject in self.multiSelect.items():
+            with self.subTest(key):
+                for dpsVal, val in self.multiSelectOptions[key].items():
+                    self.dps[self.multiSelectDps[key]] = dpsVal
+                    self.assertEqual(subject.current_option, val)
+
+    async def test_multiSelect_select_option(self):
+        for key, subject in self.multiSelect.items():
+            with self.subTest(key):
+                for dpsVal, val in self.multiSelectOptions[key].items():
+                    async with assert_device_properties_set(
+                        subject._device, {self.multiSelectDps[key]: dpsVal}
+                    ):
+                        await subject.async_select_option(val)
+
+    def test_multiSelect_device_state_attributes(self):
+        for key, subject in self.multiSelect.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_state_attributes, {})

+ 85 - 0
tests/mixins/sensor.py

@@ -0,0 +1,85 @@
+# Mixins for testing sensor entities
+
+
+class BasicSensorTests:
+    def setUpBasicSensor(
+        self,
+        dps,
+        subject,
+        unit=None,
+        state_class=None,
+        device_class=None,
+        testdata=(30, 30),
+    ):
+        self.basicSensor = subject
+        self.basicSensorDps = dps
+        self.basicSensorUnit = unit
+        self.basicSensorStateClass = state_class
+        self.basicSensorDeviceClass = device_class
+        self.basicSensorTestData = testdata
+
+    def test_basic_sensor_units(self):
+        self.assertEqual(
+            self.basicSensor.native_unit_of_measurement, self.basicSensorUnit
+        )
+
+    def test_basic_sensor_device_class(self):
+        self.assertEqual(self.basicSensor.device_class, self.basicSensorDeviceClass)
+
+    def test_basic_sensor_state_class(self):
+        self.assertEqual(self.basicSensor.state_class, self.basicSensorStateClass)
+
+    def test_basic_sensor_value(self):
+        dpval, val = self.basicSensorTestData
+        self.dps[self.basicSensorDps] = dpval
+        self.assertEqual(self.basicSensor.native_value, val)
+
+
+class MultiSensorTests:
+    def setUpMultiSensors(self, sensors):
+        self.multiSensor = {}
+        self.multiSensorDps = {}
+        self.multiSensorUnit = {}
+        self.multiSensorDevClass = {}
+        self.multiSensorStateClass = {}
+        self.multiSensorTestData = {}
+        for s in sensors:
+            name = s.get("name")
+            subject = self.entities.get(name)
+            if subject is None:
+                raise AttributeError(f"No sensor for {name} found.")
+            self.multiSensor[name] = subject
+            self.multiSensorDps[name] = s.get("dps")
+            self.multiSensorUnit[name] = s.get("unit")
+            self.multiSensorStateClass[name] = s.get("state_class")
+            self.multiSensorDevClass[name] = s.get("device_class")
+            self.multiSensorTestData[name] = s.get("testdata", (30, 30))
+
+    def test_multi_sensor_units(self):
+        for key, subject in self.multiSensor.items():
+            with self.subTest(key):
+                self.assertEqual(
+                    subject.native_unit_of_measurement, self.multiSensorUnit[key]
+                )
+
+    def test_multi_sensor_device_class(self):
+        for key, subject in self.multiSensor.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_class, self.multiSensorDevClass[key])
+
+    def test_multi_sensor_state_class(self):
+        for key, subject in self.multiSensor.items():
+            with self.subTest(key):
+                self.assertEqual(subject.state_class, self.multiSensorStateClass[key])
+
+    def test_multi_sensor_value(self):
+        for key, subject in self.multiSensor.items():
+            with self.subTest(key):
+                dpsval, val = self.multiSensorTestData[key]
+                self.dps[self.multiSensorDps[key]] = dpsval
+                self.assertEqual(subject.native_value, val)
+
+    def test_multi_sensor_device_state_attributes(self):
+        for key, subject in self.multiSensor.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_state_attributes, {})

+ 198 - 0
tests/mixins/switch.py

@@ -0,0 +1,198 @@
+# Mixins for testing switches
+from homeassistant.components.switch import DEVICE_CLASS_SWITCH
+
+from ..helpers import assert_device_properties_set
+
+
+class SwitchableTests:
+    def setUpSwitchable(self, dps, subject):
+        self.switch_dps = dps
+        self.switch_subject = subject
+
+    def test_switchable_is_on(self):
+        self.dps[self.switch_dps] = True
+        self.assertTrue(self.switch_subject.is_on)
+
+        self.dps[self.switch_dps] = False
+        self.assertFalse(self.switch_subject.is_on)
+
+        self.dps[self.switch_dps] = None
+        self.assertIsNone(self.switch_subject.is_on)
+
+    async def test_switchable_turn_on(self):
+        async with assert_device_properties_set(
+            self.switch_subject._device, {self.switch_dps: True}
+        ):
+            await self.switch_subject.async_turn_on()
+
+    async def test_switchable_turn_off(self):
+        async with assert_device_properties_set(
+            self.switch_subject._device, {self.switch_dps: False}
+        ):
+            await self.switch_subject.async_turn_off()
+
+    async def test_switchable_toggle(self):
+        self.dps[self.switch_dps] = False
+        async with assert_device_properties_set(
+            self.switch_subject._device, {self.switch_dps: True}
+        ):
+            await self.switch_subject.async_toggle()
+
+        self.dps[self.switch_dps] = True
+        async with assert_device_properties_set(
+            self.switch_subject._device, {self.switch_dps: False}
+        ):
+            await self.switch_subject.async_toggle()
+
+
+class BasicSwitchTests:
+    def setUpBasicSwitch(
+        self,
+        dps,
+        subject,
+        device_class=DEVICE_CLASS_SWITCH,
+        power_dps=None,
+        power_scale=1,
+    ):
+        self.basicSwitch = subject
+        self.basicSwitchDps = dps
+        self.basicSwitchDevClass = device_class
+        self.basicSwitchPowerDps = power_dps
+        self.basicSwitchPowerScale = power_scale
+
+    def test_basic_switch_is_on(self):
+        self.dps[self.basicSwitchDps] = True
+        self.assertEqual(self.basicSwitch.is_on, True)
+
+        self.dps[self.basicSwitchDps] = False
+        self.assertEqual(self.basicSwitch.is_on, False)
+
+    async def test_basic_switch_turn_on(self):
+        async with assert_device_properties_set(
+            self.basicSwitch._device, {self.basicSwitchDps: True}
+        ):
+            await self.basicSwitch.async_turn_on()
+
+    async def test_basic_switch_turn_off(self):
+        async with assert_device_properties_set(
+            self.basicSwitch._device, {self.basicSwitchDps: False}
+        ):
+            await self.basicSwitch.async_turn_off()
+
+    async def test_basic_switch_toggle_turns_on_when_it_was_off(self):
+        self.dps[self.basicSwitchDps] = False
+
+        async with assert_device_properties_set(
+            self.basicSwitch._device, {self.basicSwitchDps: True}
+        ):
+            await self.basicSwitch.async_toggle()
+
+    async def test_basic_switch_toggle_turns_off_when_it_was_on(self):
+        self.dps[self.basicSwitchDps] = True
+
+        async with assert_device_properties_set(
+            self.basicSwitch._device, {self.basicSwitchDps: False}
+        ):
+            await self.basicSwitch.async_toggle()
+
+    def test_basic_switch_class_device_class(self):
+        self.assertEqual(self.basicSwitch.device_class, self.basicSwitchDevClass)
+
+    def test_basic_switch_current_power_w(self):
+        if self.basicSwitchPowerDps is None:
+            self.assertIsNone(self.basicSwitch.current_power_w)
+        else:
+            self.dps[self.basicSwitchPowerDps] = 123
+            self.assertEqual(
+                self.basicSwitch.current_power_w, 123 / self.basicSwitchPowerScale
+            )
+
+    def test_basic_switch_state_attributes(self):
+        self.assertEqual(self.basicSwitch.device_state_attributes, {})
+
+
+class MultiSwitchTests:
+    def setUpMultiSwitch(self, switches):
+        self.multiSwitch = {}
+        self.multiSwitchDps = {}
+        self.multiSwitchDevClass = {}
+        self.multiSwitchPowerDps = {}
+        self.multiSwitchPowerScale = {}
+
+        for s in switches:
+            name = s.get("name")
+            subject = self.entities.get(name)
+            if subject is None:
+                raise AttributeError(f"No switch for {name} found.")
+            self.multiSwitch[name] = subject
+            self.multiSwitchDps[name] = s.get("dps")
+            self.multiSwitchDevClass[name] = s.get("device_class", DEVICE_CLASS_SWITCH)
+            self.multiSwitchPowerDps[name] = s.get("power_dps")
+            self.multiSwitchPowerScale[name] = s.get("power_scale", 1)
+
+    def test_multi_switch_is_on(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                dp = self.multiSwitchDps[key]
+                self.dps[dp] = True
+                self.assertEqual(subject.is_on, True)
+
+                self.dps[dp] = False
+                self.assertEqual(subject.is_on, False)
+
+    async def test_multi_switch_turn_on(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                async with assert_device_properties_set(
+                    subject._device, {self.multiSwitchDps[key]: True}
+                ):
+                    await subject.async_turn_on()
+
+    async def test_multi_switch_turn_off(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                async with assert_device_properties_set(
+                    subject._device, {self.multiSwitchDps[key]: False}
+                ):
+                    await subject.async_turn_off()
+
+    async def test_multi_switch_toggle_turns_on_when_it_was_off(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                dp = self.multiSwitchDps[key]
+                self.dps[dp] = False
+
+                async with assert_device_properties_set(subject._device, {dp: True}):
+                    await subject.async_toggle()
+
+    async def test_multi_switch_toggle_turns_off_when_it_was_on(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                dp = self.multiSwitchDps[key]
+                self.dps[dp] = True
+
+                async with assert_device_properties_set(subject._device, {dp: False}):
+                    await subject.async_toggle()
+
+    def test_multi_switch_device_class(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_class, self.multiSwitchDevClass[key])
+
+    def test_multi_switch_current_power_w(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                dp = self.multiSwitchPowerDps.get(key)
+                if dp is None:
+                    self.assertIsNone(subject.current_power_w)
+                else:
+                    self.dps[dp] = 1234
+                    self.assertEqual(
+                        subject.current_power_w,
+                        1234 / self.multiSwitchPowerScale.get(key, 1),
+                    )
+
+    def test_multi_switch_state_attributes(self):
+        for key, subject in self.multiSwitch.items():
+            with self.subTest(key):
+                self.assertEqual(subject.device_state_attributes, {})

+ 5 - 0
tests/test_config_flow.py

@@ -432,6 +432,7 @@ async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
                 CONF_NAME: "test",
                 CONF_CLIMATE: True,
                 "lock_child_lock": False,
+                "number_timer": False,
             },
         )
         expected = {
@@ -450,6 +451,7 @@ async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
                 CONF_HOST: "hostname",
                 CONF_LOCAL_KEY: "localkey",
                 "lock_child_lock": False,
+                "number_timer": False,
                 CONF_TYPE: "kogan_kahtp_heater",
             },
         }
@@ -505,6 +507,7 @@ async def test_options_flow_modifies_config(mock_test, hass):
             CONF_HOST: "hostname",
             CONF_LOCAL_KEY: "localkey",
             "lock_child_lock": True,
+            "number_timer": False,
             CONF_NAME: "test",
             CONF_TYPE: "kogan_kahtp_heater",
         },
@@ -523,6 +526,7 @@ async def test_options_flow_modifies_config(mock_test, hass):
             CONF_HOST: "new_hostname",
             CONF_LOCAL_KEY: "new_key",
             "lock_child_lock": False,
+            "number_timer": True,
         },
     )
     expected = {
@@ -530,6 +534,7 @@ async def test_options_flow_modifies_config(mock_test, hass):
         CONF_HOST: "new_hostname",
         CONF_LOCAL_KEY: "new_key",
         "lock_child_lock": False,
+        "number_timer": True,
     }
     assert "create_entry" == result["type"]
     assert "" == result["title"]