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

Add units to number entities.

Based on release notes for 2021.12, it is also possible to set units on number entities.  It may improve the UI somehow.
Jason Rumney 4 лет назад
Родитель
Сommit
05839e8486
51 измененных файлов с 238 добавлено и 54 удалено
  1. 1 0
      custom_components/tuya_local/devices/anko_fan.yaml
  2. 2 0
      custom_components/tuya_local/devices/awow_th213_thermostat.yaml
  3. 2 0
      custom_components/tuya_local/devices/digoo_dgsp202.yaml
  4. 1 0
      custom_components/tuya_local/devices/eberg_qubo_q40hd_heatpump.yaml
  5. 1 0
      custom_components/tuya_local/devices/goldair_dehumidifier.yaml
  6. 1 0
      custom_components/tuya_local/devices/goldair_geco_heater.yaml
  7. 1 0
      custom_components/tuya_local/devices/goldair_gpcv_heater.yaml
  8. 1 0
      custom_components/tuya_local/devices/goldair_gpph_heater.yaml
  9. 2 0
      custom_components/tuya_local/devices/grid_connect_usb_double_power_point.yaml
  10. 1 0
      custom_components/tuya_local/devices/inkbird_itc306a_thermostat.yaml
  11. 1 0
      custom_components/tuya_local/devices/kogan_kahtp_heater.yaml
  12. 1 0
      custom_components/tuya_local/devices/kogan_kawfhtp_heater.yaml
  13. 1 0
      custom_components/tuya_local/devices/lexy_f501_fan.yaml
  14. 25 1
      custom_components/tuya_local/devices/minco_mh1823d_thermostat.yaml
  15. 1 0
      custom_components/tuya_local/devices/nedis_htpl20f_heater.yaml
  16. 2 0
      custom_components/tuya_local/devices/qoto_03_sprinkler.yaml
  17. 2 0
      custom_components/tuya_local/devices/saswell_c16_thermostat.yaml
  18. 1 0
      custom_components/tuya_local/devices/simple_switch_timer.yaml
  19. 1 0
      custom_components/tuya_local/devices/smartplugv1.yaml
  20. 1 0
      custom_components/tuya_local/devices/smartplugv2.yaml
  21. 1 0
      custom_components/tuya_local/devices/smartplugv2_energy.yaml
  22. 1 0
      custom_components/tuya_local/devices/tmwf02_fan.yaml
  23. 4 0
      custom_components/tuya_local/devices/woox_r4028_powerstrip.yaml
  24. 14 16
      custom_components/tuya_local/generic/climate.py
  25. 12 1
      custom_components/tuya_local/generic/number.py
  26. 7 10
      custom_components/tuya_local/generic/sensor.py
  27. 14 0
      custom_components/tuya_local/helpers/mixin.py
  28. 7 1
      tests/devices/test_anko_fan.py
  29. 2 0
      tests/devices/test_awow_th213_thermostat.py
  30. 3 0
      tests/devices/test_digoo_dgsp202.py
  31. 12 2
      tests/devices/test_eberg_qubo_q40hd_heatpump.py
  32. 7 1
      tests/devices/test_goldair_dehumidifier.py
  33. 2 2
      tests/devices/test_goldair_geco_heater.py
  34. 2 2
      tests/devices/test_goldair_gpcv_heater.py
  35. 6 1
      tests/devices/test_goldair_gpph_heater.py
  36. 3 0
      tests/devices/test_grid_connect_double_power_point.py
  37. 5 1
      tests/devices/test_inkbird_itc306a_thermostat.py
  38. 7 2
      tests/devices/test_kogan_kahtp_heater.py
  39. 7 2
      tests/devices/test_kogan_kawfhtp_heater.py
  40. 7 1
      tests/devices/test_lexy_f501_fan.py
  41. 4 0
      tests/devices/test_minco_mh1823d_thermostat.py
  42. 7 2
      tests/devices/test_nedis_htpl20f_heater.py
  43. 4 1
      tests/devices/test_qoto_03_sprinkler.py
  44. 3 1
      tests/devices/test_saswell_c16_thermostat.py
  45. 6 1
      tests/devices/test_simple_switch_with_timer.py
  46. 7 1
      tests/devices/test_smartplugv1.py
  47. 7 1
      tests/devices/test_smartplugv2.py
  48. 7 1
      tests/devices/test_smartplugv2_energy.py
  49. 2 2
      tests/devices/test_tmwf02_fan.py
  50. 5 0
      tests/devices/test_woox_r4028_powerstrip.py
  51. 14 1
      tests/mixins/number.py

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

@@ -43,6 +43,7 @@ secondary_entities:
       - id: 6
         type: integer
         name: value
+        unit: s
         range:
           min: 0
           max: 9

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

@@ -135,6 +135,7 @@ secondary_entities:
       - id: 103
         type: integer
         name: value
+        unit: C
         range:
           min: -9
           max: 9
@@ -146,6 +147,7 @@ secondary_entities:
       - id: 104
         type: integer
         name: value
+        unit: C
         range:
           min: 1
           max: 9

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

@@ -63,6 +63,7 @@ secondary_entities:
       - id: 9
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400
@@ -77,6 +78,7 @@ secondary_entities:
       - id: 10
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400

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

@@ -132,6 +132,7 @@ secondary_entities:
       - id: 22
         type: integer
         name: value
+        unit: h
         range:
           min: 0
           max: 24

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

@@ -297,6 +297,7 @@ secondary_entities:
       - id: 12
         name: value
         type: integer
+        unit: h
         range:
           min: 0
           max: 24

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

@@ -54,6 +54,7 @@ secondary_entities:
       - id: 5
         type: integer
         name: value
+        unit: h
         range:
           min: 0
           max: 24

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

@@ -62,6 +62,7 @@ secondary_entities:
       - id: 5
         type: integer
         name: value
+        unit: h
         range:
           min: 0
           max: 24

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

@@ -158,6 +158,7 @@ secondary_entities:
       - id: 102
         type: integer
         name: value
+        unit: min
         range:
           min: 0
           max: 1440

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

@@ -170,6 +170,7 @@ secondary_entities:
       - id: 9
         type: integer
         name: value
+        unit: s
         range:
           min: 0
           max: 86400
@@ -181,6 +182,7 @@ secondary_entities:
       - id: 10
         type: integer
         name: value
+        unit: s
         range:
           min: 0
           max: 86400

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

@@ -155,6 +155,7 @@ secondary_entities:
       - id: 108
         type: integer
         name: value
+        unit: h
         range:
           min: 0
           max: 96

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

@@ -55,6 +55,7 @@ secondary_entities:
       - id: 8
         name: value
         type: integer
+        unit: h
         range:
           min: 0
           max: 24

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

@@ -54,6 +54,7 @@ secondary_entities:
       - id: 5
         type: integer
         name: value
+        unit: h
         range:
           min: 0
           max: 24

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

@@ -90,6 +90,7 @@ secondary_entities:
       - id: 6
         type: integer
         name: value
+        unit: h
         range:
           min: 0
           max: 7

+ 25 - 1
custom_components/tuya_local/devices/minco_mh1823d_thermostat.yaml

@@ -155,6 +155,15 @@ secondary_entities:
                 range:
                   min: -15
                   max: 15
+      - id: 19
+        type: string
+        name: unit
+        hidden: true
+        mapping:
+          - dps_val: c
+            value: C
+          - dps_val: f
+            value: F
   - entity: number
     name: Calibration Offset External
     category: config
@@ -177,6 +186,11 @@ secondary_entities:
         type: string
         name: unit
         hidden: true
+        mapping:
+          - dps_val: c
+            value: C
+          - dps_val: f
+            value: F
   - entity: number
     name: Calibration Swing
     category: config
@@ -199,6 +213,11 @@ secondary_entities:
         type: string
         name: unit
         hidden: true
+        mapping:
+          - dps_val: c
+            value: C
+          - dps_val: f
+            value: F
   - entity: number
     category: config
     name: High Temperature Limit
@@ -227,7 +246,12 @@ secondary_entities:
       - id: 19
         type: string
         name: unit
-        hidden: true        
+        hidden: true
+        mapping:
+          - dps_val: c
+            value: C
+          - dps_val: f
+            value: F
   - entity: select
     name: Schedule
     category: config

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

@@ -57,6 +57,7 @@ secondary_entities:
       - id: 13
         type: integer
         name: value
+        unit: min
         range:
           min: 0
           max: 1440

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

@@ -6,6 +6,7 @@ primary_entity:
     - id: 102
       type: integer
       name: value
+      unit: "%"
       range:
         min: 0
         max: 100
@@ -38,6 +39,7 @@ secondary_entities:
       - id: 105
         type: integer
         name: value
+        unit: s
         range:
           min: 0
           max: 86399

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

@@ -119,6 +119,7 @@ secondary_entities:
       - id: 6
         name: value
         type: integer
+        unit: C
         range:
           min: 200
           max: 500
@@ -180,6 +181,7 @@ secondary_entities:
       - id: 22
         name: value
         type: integer
+        unit: W
         range:
           min: 0
           max: 3500

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

@@ -15,6 +15,7 @@ secondary_entities:
       - id: 11
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400

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

@@ -71,6 +71,7 @@ secondary_entities:
       - id: 2
         type: integer
         name: value
+        unit: min
         range:
           min: 0
           max: 1440

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

@@ -71,6 +71,7 @@ secondary_entities:
       - id: 9
         type: integer
         name: value
+        unit: min
         range:
           min: 0
           max: 1440

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

@@ -39,6 +39,7 @@ secondary_entities:
       - id: 9
         type: integer
         name: value
+        unit: min
         range:
           min: 0
           max: 1440

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

@@ -23,6 +23,7 @@ secondary_entities:
       - id: 2
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400

+ 4 - 0
custom_components/tuya_local/devices/woox_r4028_powerstrip.yaml

@@ -37,6 +37,7 @@ secondary_entities:
       - id: 101
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400
@@ -51,6 +52,7 @@ secondary_entities:
       - id: 102
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400
@@ -65,6 +67,7 @@ secondary_entities:
       - id: 103
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400
@@ -79,6 +82,7 @@ secondary_entities:
       - id: 105
         name: value
         type: integer
+        unit: min
         range:
           min: 0
           max: 86400

+ 14 - 16
custom_components/tuya_local/generic/climate.py

@@ -39,10 +39,17 @@ from homeassistant.const import (
 
 from ..device import TuyaLocalDevice
 from ..helpers.device_config import TuyaEntityConfig
-from ..helpers.mixin import TuyaLocalEntity
+from ..helpers.mixin import TuyaLocalEntity, unit_from_ascii
 
 _LOGGER = logging.getLogger(__name__)
 
+VALID_TEMP_UNIT = [TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN]
+
+
+def validate_temp_unit(unit):
+    unit = unit_from_ascii(unit)
+    return unit if unit in VALID_TEMP_UNIT else None
+
 
 class TuyaLocalClimate(TuyaLocalEntity, ClimateEntity):
     """Representation of a Tuya Climate entity."""
@@ -101,24 +108,15 @@ class TuyaLocalClimate(TuyaLocalEntity, ClimateEntity):
         """Return the unit of measurement."""
         # If there is a separate DPS that returns the units, use that
         if self._unit_dps is not None:
-            unit = self._unit_dps.get_value(self._device)
+            unit = validate_temp_unit(self._unit_dps.get_value(self._device))
             # Only return valid units
-            if unit == "C":
-                return TEMP_CELSIUS
-            elif unit == "F":
-                return TEMP_FAHRENHEIT
-            elif unit == "K":
-                return TEMP_KELVIN
+            if unit is not None:
+                return unit
         # If there unit attribute configured in the temperature dps, use that
         if self._temperature_dps:
-            unit = self._temperature_dps.unit
-            # Only return valid units
-            if unit == "C":
-                return TEMP_CELSIUS
-            elif unit == "F":
-                return TEMP_FAHRENHEIT
-            elif unit == "K":
-                return TEMP_KELVIN
+            unit = validate_temp_unit(self._temperature_dps.unit)
+            if unit is not None:
+                return unit
         # Return the default unit from the device
         return self._device.temperature_unit
 

+ 12 - 1
custom_components/tuya_local/generic/number.py

@@ -9,7 +9,7 @@ from homeassistant.components.number.const import (
 
 from ..device import TuyaLocalDevice
 from ..helpers.device_config import TuyaEntityConfig
-from ..helpers.mixin import TuyaLocalEntity
+from ..helpers.mixin import TuyaLocalEntity, unit_from_ascii
 
 MODE_AUTO = "auto"
 
@@ -28,6 +28,7 @@ class TuyaLocalNumber(TuyaLocalEntity, NumberEntity):
         self._value_dps = dps_map.pop("value")
         if self._value_dps is None:
             raise AttributeError(f"{config.name} is missing a value dps")
+        self._unit_dps = dps_map.pop("unit", None)
         self._init_end(dps_map)
 
     @property
@@ -52,6 +53,16 @@ class TuyaLocalNumber(TuyaLocalEntity, NumberEntity):
             m = MODE_AUTO
         return m
 
+    @property
+    def unit_of_measurement(self):
+        """Return the unit associated with this number."""
+        if self._unit_dps is None:
+            unit = self._value_dps.unit
+        else:
+            unit = self._unit_dps.get_value(self._device)
+
+        return unit_from_ascii(unit)
+
     @property
     def value(self):
         """Return the current value of the number."""

+ 7 - 10
custom_components/tuya_local/generic/sensor.py

@@ -1,12 +1,15 @@
 """
 Platform to read Tuya sensors.
 """
-from homeassistant.components.sensor import DEVICE_CLASSES, SensorEntity, STATE_CLASSES
-from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.components.sensor import (
+    DEVICE_CLASSES,
+    SensorEntity,
+    STATE_CLASSES,
+)
 
 from ..device import TuyaLocalDevice
 from ..helpers.device_config import TuyaEntityConfig
-from ..helpers.mixin import TuyaLocalEntity
+from ..helpers.mixin import TuyaLocalEntity, unit_from_ascii
 
 
 class TuyaLocalSensor(TuyaLocalEntity, SensorEntity):
@@ -58,10 +61,4 @@ class TuyaLocalSensor(TuyaLocalEntity, SensorEntity):
         else:
             unit = self._unit_dps.get_value(self._device)
 
-        # Temperatures use Unicode characters, translate from simpler ASCII
-        if unit == "C":
-            unit = TEMP_CELSIUS
-        elif unit == "F":
-            unit = TEMP_FAHRENHEIT
-
-        return unit
+        return unit_from_ascii(unit)

+ 14 - 0
custom_components/tuya_local/helpers/mixin.py

@@ -2,6 +2,7 @@
 Mixins to make writing new platforms easier
 """
 import logging
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -67,3 +68,16 @@ class TuyaLocalEntity:
 
     async def async_update(self):
         await self._device.async_refresh()
+
+
+UNIT_ASCII_MAP = {
+    "C": TEMP_CELSIUS,
+    "F": TEMP_FAHRENHEIT,
+}
+
+
+def unit_from_ascii(unit):
+    if unit in UNIT_ASCII_MAP:
+        return UNIT_ASCII_MAP[unit]
+
+    return unit

+ 7 - 1
tests/devices/test_anko_fan.py

@@ -3,6 +3,7 @@ from homeassistant.components.fan import (
     SUPPORT_PRESET_MODE,
     SUPPORT_SET_SPEED,
 )
+from homeassistant.const import TIME_SECONDS
 
 from ..const import ANKO_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -24,7 +25,12 @@ class TestAnkoFan(SwitchableTests, BasicNumberTests, TuyaDeviceTestCase):
         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)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=9,
+            unit=TIME_SECONDS,
+        )
         self.mark_secondary(["number_timer"])
 
     def test_supported_features(self):

+ 2 - 0
tests/devices/test_awow_th213_thermostat.py

@@ -81,12 +81,14 @@ class TestAwowTH213Thermostat(
                     "dps": CALIBRATE_DPS,
                     "min": -9,
                     "max": 9,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_calibration_swing",
                     "dps": CALIBSWING_DPS,
                     "min": 1,
                     "max": 9,
+                    "unit": TEMP_CELSIUS,
                 },
             ]
         )

+ 3 - 0
tests/devices/test_digoo_dgsp202.py

@@ -7,6 +7,7 @@ from homeassistant.const import (
     ELECTRIC_CURRENT_MILLIAMPERE,
     ELECTRIC_POTENTIAL_VOLT,
     POWER_WATT,
+    TIME_MINUTES,
 )
 
 from ..const import DIGOO_DGSP202_SOCKET_PAYLOAD
@@ -54,12 +55,14 @@ class TestDigooDGSP202Switch(
                     "name": "number_timer_1",
                     "max": 1440,
                     "scale": 60,
+                    "unit": TIME_MINUTES,
                 },
                 {
                     "dps": TIMER2_DPS,
                     "name": "number_timer_2",
                     "max": 1440,
                     "scale": 60,
+                    "unit": TIME_MINUTES,
                 },
             ]
         )

+ 12 - 2
tests/devices/test_eberg_qubo_q40hd_heatpump.py

@@ -17,7 +17,12 @@ from homeassistant.components.climate.const import (
     SWING_OFF,
     SWING_VERTICAL,
 )
-from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.const import (
+    STATE_UNAVAILABLE,
+    TEMP_CELSIUS,
+    TEMP_FAHRENHEIT,
+    TIME_HOURS,
+)
 
 from ..const import EBERG_QUBO_Q40HD_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -54,7 +59,12 @@ class TestEbergQuboQ40HDHeatpump(
             min=17,
             max=30,
         )
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=24,
+            unit=TIME_HOURS,
+        )
         self.mark_secondary(["number_timer"])
 
     def test_supported_features(self):

+ 7 - 1
tests/devices/test_goldair_dehumidifier.py

@@ -19,6 +19,7 @@ from homeassistant.const import (
     DEVICE_CLASS_TEMPERATURE,
     STATE_UNAVAILABLE,
     TEMP_CELSIUS,
+    TIME_HOURS,
 )
 
 from ..const import DEHUMIDIFIER_PAYLOAD
@@ -73,7 +74,12 @@ 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.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=24,
+            unit=TIME_HOURS,
+        )
 
         self.setUpMultiSensors(
             [

+ 2 - 2
tests/devices/test_goldair_geco_heater.py

@@ -4,7 +4,7 @@ from homeassistant.components.climate.const import (
     HVAC_MODE_OFF,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TIME_HOURS
 
 from ..const import GECO_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -41,7 +41,7 @@ class TestGoldairGECOHeater(
             max=35,
         )
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24, unit=TIME_HOURS)
         self.setUpBasicBinarySensor(
             ERROR_DPS,
             self.entities.get("binary_sensor_error"),

+ 2 - 2
tests/devices/test_goldair_gpcv_heater.py

@@ -5,7 +5,7 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TIME_HOURS
 
 from ..const import GPCV_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -43,7 +43,7 @@ class TestGoldairGPCVHeater(
             max=35,
         )
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24, unit=TIME_HOURS)
         self.setUpBasicBinarySensor(
             ERROR_DPS,
             self.entities.get("binary_sensor_error"),

+ 6 - 1
tests/devices/test_goldair_gpph_heater.py

@@ -11,6 +11,7 @@ from homeassistant.const import (
     DEVICE_CLASS_POWER_FACTOR,
     PERCENTAGE,
     STATE_UNAVAILABLE,
+    TIME_MINUTES,
 )
 
 from ..const import GPPH_HEATER_PAYLOAD
@@ -60,7 +61,11 @@ class TestGoldairHeater(
         self.setUpBasicLight(LIGHT_DPS, self.entities.get("light_display"))
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
         self.setUpBasicNumber(
-            TIMER_DPS, self.entities.get("number_timer"), min=0, max=1440, step=60
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            step=60,
+            unit=TIME_MINUTES,
         )
         self.setUpBasicSensor(
             POWERLEVEL_DPS,

+ 3 - 0
tests/devices/test_grid_connect_double_power_point.py

@@ -9,6 +9,7 @@ from homeassistant.const import (
     ELECTRIC_POTENTIAL_VOLT,
     ENERGY_WATT_HOUR,
     POWER_WATT,
+    TIME_SECONDS,
 )
 
 from ..const import GRIDCONNECT_2SOCKET_PAYLOAD
@@ -126,11 +127,13 @@ class TestGridConnectDoubleSwitch(
                     "name": "number_timer_1",
                     "dps": COUNTDOWN1_DPS,
                     "max": 86400,
+                    "unit": TIME_SECONDS,
                 },
                 {
                     "name": "number_timer_2",
                     "dps": COUNTDOWN2_DPS,
                     "max": 86400,
+                    "unit": TIME_SECONDS,
                 },
             ]
         )

+ 5 - 1
tests/devices/test_inkbird_itc306a_thermostat.py

@@ -9,7 +9,7 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE_RANGE,
 )
-from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS
 
 
 from ..const import INKBIRD_THERMOSTAT_PAYLOAD
@@ -95,11 +95,13 @@ class TestInkbirdThermostat(
                     "step": 0.1,
                     "min": -9.9,
                     "max": 9.9,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_continuous_heat_hours",
                     "dps": TIME_THRES_DPS,
                     "max": 96,
+                    "unit": TIME_HOURS,
                 },
                 {
                     "name": "number_high_temperature_limit",
@@ -108,6 +110,7 @@ class TestInkbirdThermostat(
                     "step": 0.1,
                     "min": -40,
                     "max": 100,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_low_temperature_limit",
@@ -116,6 +119,7 @@ class TestInkbirdThermostat(
                     "step": 0.1,
                     "min": -40,
                     "max": 100,
+                    "unit": TEMP_CELSIUS,
                 },
             ]
         )

+ 7 - 2
tests/devices/test_kogan_kahtp_heater.py

@@ -4,7 +4,7 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TIME_HOURS
 
 from ..const import KOGAN_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -36,7 +36,12 @@ class TestGoldairKoganKAHTPHeater(
             max=35,
         )
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=24,
+            unit=TIME_HOURS,
+        )
         self.mark_secondary(["lock_child_lock", "number_timer"])
 
     def test_supported_features(self):

+ 7 - 2
tests/devices/test_kogan_kawfhtp_heater.py

@@ -4,7 +4,7 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TIME_HOURS
 
 from ..const import KOGAN_KAWFHTP_HEATER_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -39,7 +39,12 @@ class TestGoldairKoganKAHTPHeater(
             max=40,
         )
         self.setUpBasicLock(LOCK_DPS, self.entities.get("lock_child_lock"))
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=24)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=24,
+            unit=TIME_HOURS,
+        )
         self.mark_secondary(["lock_child_lock", "number_timer"])
 
     def test_supported_features(self):

+ 7 - 1
tests/devices/test_lexy_f501_fan.py

@@ -3,6 +3,7 @@ from homeassistant.components.fan import (
     SUPPORT_PRESET_MODE,
     SUPPORT_SET_SPEED,
 )
+from homeassistant.const import TIME_HOURS
 
 from ..const import LEXY_F501_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -38,7 +39,12 @@ 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.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=7,
+            unit=TIME_HOURS,
+        )
         self.setUpBasicSwitch(SWITCH_DPS, self.entities.get("switch_sound"))
         self.mark_secondary(
             [

+ 4 - 0
tests/devices/test_minco_mh1823d_thermostat.py

@@ -90,24 +90,28 @@ class TestMincoMH1823DThermostat(
                     "dps": CALIBINT_DPS,
                     "min": -9,
                     "max": 9,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_calibration_offset_external",
                     "dps": CALIBEXT_DPS,
                     "min": -9,
                     "max": 9,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_calibration_swing",
                     "dps": CALIBSWING_DPS,
                     "min": 1,
                     "max": 9,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_high_temperature_limit",
                     "dps": TEMPLIMIT_DPS,
                     "min": 5,
                     "max": 65,
+                    "unit": TEMP_CELSIUS,
                 },
             ]
         )

+ 7 - 2
tests/devices/test_nedis_htpl20f_heater.py

@@ -7,7 +7,7 @@ from homeassistant.components.climate.const import (
     SUPPORT_TARGET_TEMPERATURE,
     SUPPORT_PRESET_MODE,
 )
-from homeassistant.const import STATE_UNAVAILABLE
+from homeassistant.const import STATE_UNAVAILABLE, TIME_MINUTES
 
 from ..const import NEDIS_HTPL20F_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -44,7 +44,12 @@ class TestNedisHtpl20fHeater(
             LOCK_DPS,
             self.entities.get("lock_child_lock"),
         )
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=1440)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            unit=TIME_MINUTES,
+        )
         self.mark_secondary(["lock_child_lock", "number_timer"])
 
     def test_supported_features(self):

+ 4 - 1
tests/devices/test_qoto_03_sprinkler.py

@@ -1,5 +1,6 @@
 """Tests for the Quto 03 Sprinkler."""
 from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM
+from homeassistant.const import PERCENTAGE, TIME_SECONDS
 
 from ..const import QOTO_SPRINKLER_PAYLOAD
 from ..mixins.binary_sensor import BasicBinarySensorTests
@@ -14,7 +15,7 @@ TIMER_DPS = "105"
 ERROR_DPS = "108"
 
 
-class TestQoboSprinkler(
+class TestQotoSprinkler(
     BasicBinarySensorTests,
     MultiNumberTests,
     MultiSensorTests,
@@ -37,11 +38,13 @@ class TestQoboSprinkler(
                     "dps": TARGET_DPS,
                     "max": 100,
                     "step": 5,
+                    "unit": PERCENTAGE,
                 },
                 {
                     "name": "number_timer",
                     "dps": TIMER_DPS,
                     "max": 86399,
+                    "unit": TIME_SECONDS,
                 },
             ]
         )

+ 3 - 1
tests/devices/test_saswell_c16_thermostat.py

@@ -9,7 +9,7 @@ from homeassistant.components.climate.const import (
     SUPPORT_PRESET_MODE,
     SUPPORT_TARGET_TEMPERATURE,
 )
-from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, POWER_WATT, TEMP_CELSIUS
 
 from ..const import SASWELL_C16_THERMOSTAT_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -85,11 +85,13 @@ class TestSaswellC16Thermostat(
                     "max": 50.0,
                     "scale": 10,
                     "step": 0.5,
+                    "unit": TEMP_CELSIUS,
                 },
                 {
                     "name": "number_power_rating",
                     "dps": POWERRATING_DPS,
                     "max": 3500,
+                    "unit": POWER_WATT,
                 },
             ]
         )

+ 6 - 1
tests/devices/test_simple_switch_with_timer.py

@@ -1,5 +1,6 @@
 """Tests for a simple switch with timer"""
 from homeassistant.components.switch import DEVICE_CLASS_OUTLET
+from homeassistant.const import TIME_MINUTES
 
 from ..const import TIMED_SOCKET_PAYLOAD
 from ..mixins.number import BasicNumberTests
@@ -18,7 +19,11 @@ class TestTimedSwitch(BasicNumberTests, SwitchableTests, TuyaDeviceTestCase):
         self.subject = self.entities.get("switch")
         self.setUpSwitchable(SWITCH_DPS, self.subject)
         self.setUpBasicNumber(
-            TIMER_DPS, self.entities.get("number_timer"), max=1440, scale=60
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            scale=60,
+            unit=TIME_MINUTES,
         )
         self.mark_secondary(["number_timer"])
 

+ 7 - 1
tests/devices/test_smartplugv1.py

@@ -7,6 +7,7 @@ from homeassistant.const import (
     ELECTRIC_CURRENT_MILLIAMPERE,
     ELECTRIC_POTENTIAL_VOLT,
     POWER_WATT,
+    TIME_MINUTES,
 )
 
 from ..const import KOGAN_SOCKET_PAYLOAD
@@ -31,7 +32,12 @@ class TestKoganSwitch(
         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.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            unit=TIME_MINUTES,
+        )
         self.setUpMultiSensors(
             [
                 {

+ 7 - 1
tests/devices/test_smartplugv2.py

@@ -7,6 +7,7 @@ from homeassistant.const import (
     ELECTRIC_CURRENT_MILLIAMPERE,
     ELECTRIC_POTENTIAL_VOLT,
     POWER_WATT,
+    TIME_MINUTES,
 )
 
 from ..const import KOGAN_SOCKET_PAYLOAD2
@@ -31,7 +32,12 @@ class TestSwitchV2(
         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.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            unit=TIME_MINUTES,
+        )
         self.setUpMultiSensors(
             [
                 {

+ 7 - 1
tests/devices/test_smartplugv2_energy.py

@@ -10,6 +10,7 @@ from homeassistant.const import (
     ELECTRIC_POTENTIAL_VOLT,
     ENERGY_WATT_HOUR,
     POWER_WATT,
+    TIME_MINUTES,
 )
 
 from ..const import SMARTSWITCH_ENERGY_PAYLOAD
@@ -69,7 +70,12 @@ class TestSwitchV2Energy(
             device_class=DEVICE_CLASS_PROBLEM,
             testdata=(1, 0),
         )
-        self.setUpBasicNumber(TIMER_DPS, self.entities.get("number_timer"), max=1440)
+        self.setUpBasicNumber(
+            TIMER_DPS,
+            self.entities.get("number_timer"),
+            max=1440,
+            unit=TIME_MINUTES,
+        )
         self.setUpBasicSelect(
             INITIAL_DPS,
             self.entities.get("select_initial_state"),

+ 2 - 2
tests/devices/test_tmwf02_fan.py

@@ -1,7 +1,6 @@
 from homeassistant.components.fan import SUPPORT_SET_SPEED
 from homeassistant.const import (
-    DEVICE_CLASS_POWER_FACTOR,
-    PERCENTAGE,
+    TIME_MINUTES,
 )
 from ..const import TMWF02_FAN_PAYLOAD
 from ..helpers import assert_device_properties_set
@@ -27,6 +26,7 @@ class TestTMWF02Fan(BasicNumberTests, SwitchableTests, TuyaDeviceTestCase):
             self.entities.get("number_timer"),
             max=1440,
             scale=60,
+            unit=TIME_MINUTES,
         )
         self.mark_secondary(["number_timer"])
 

+ 5 - 0
tests/devices/test_woox_r4028_powerstrip.py

@@ -1,5 +1,6 @@
 """Tests for the Woox R4028 powerstrip."""
 from homeassistant.components.switch import DEVICE_CLASS_OUTLET
+from homeassistant.const import TIME_MINUTES
 
 from ..const import WOOX_R4028_SOCKET_PAYLOAD
 from ..mixins.number import MultiNumberTests
@@ -52,24 +53,28 @@ class TestWooxR4028Powerstrip(
                     "name": "number_timer_1",
                     "max": 1440,
                     "scale": 60,
+                    "unit": TIME_MINUTES,
                 },
                 {
                     "dps": TIMER2_DPS,
                     "name": "number_timer_2",
                     "max": 1440,
                     "scale": 60,
+                    "unit": TIME_MINUTES,
                 },
                 {
                     "dps": TIMER3_DPS,
                     "name": "number_timer_3",
                     "max": 1440,
                     "scale": 60,
+                    "unit": TIME_MINUTES,
                 },
                 {
                     "dps": TIMERUSB_DPS,
                     "name": "number_usb_timer",
                     "max": 1440,
                     "scale": 60,
+                    "unit": TIME_MINUTES,
                 },
             ]
         )

+ 14 - 1
tests/mixins/number.py

@@ -3,7 +3,9 @@ from ..helpers import assert_device_properties_set
 
 
 class BasicNumberTests:
-    def setUpBasicNumber(self, dps, subject, max, min=0, step=1, mode="auto", scale=1):
+    def setUpBasicNumber(
+        self, dps, subject, max, min=0, step=1, mode="auto", scale=1, unit=None
+    ):
         self.basicNumber = subject
         self.basicNumberDps = dps
         self.basicNumberMin = min
@@ -11,6 +13,7 @@ class BasicNumberTests:
         self.basicNumberStep = step
         self.basicNumberMode = mode
         self.basicNumberScale = scale
+        self.basicNumberUnit = unit
 
     def test_number_min_value(self):
         self.assertEqual(self.basicNumber.min_value, self.basicNumberMin)
@@ -24,6 +27,9 @@ class BasicNumberTests:
     def test_number_mode(self):
         self.assertEqual(self.basicNumber.mode, self.basicNumberMode)
 
+    def test_number_unit_of_measurement(self):
+        self.assertEqual(self.basicNumber.unit_of_measurement, self.basicNumberUnit)
+
     def test_number_value(self):
         val = min(max(self.basicNumberMin, self.basicNumberStep), self.basicNumberMax)
         dps_val = val * self.basicNumberScale
@@ -51,6 +57,7 @@ class MultiNumberTests:
         self.multiNumberStep = {}
         self.multiNumberMode = {}
         self.multiNumberScale = {}
+        self.multiNumberUnit = {}
 
         for n in numbers:
             name = n.get("name")
@@ -64,6 +71,7 @@ class MultiNumberTests:
             self.multiNumberStep[name] = n.get("step", 1)
             self.multiNumberMode[name] = n.get("mode", "auto")
             self.multiNumberScale[name] = n.get("scale", 1)
+            self.multiNumberUnit[name] = n.get("unit", None)
 
     def test_multi_number_min_value(self):
         for key, subject in self.multiNumber.items():
@@ -85,6 +93,11 @@ class MultiNumberTests:
             with self.subTest(key):
                 self.assertEqual(subject.mode, self.multiNumberMode[key])
 
+    def test_multi_number_unit_of_measurement(self):
+        for key, subject in self.multiNumber.items():
+            with self.subTest(key):
+                self.assertEqual(subject.unit_of_measurement, self.multiNumberUnit[key])
+
     def test_multi_number_value(self):
         for key, subject in self.multiNumber.items():
             with self.subTest(key):