Bläddra i källkod

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 år sedan
förälder
incheckning
05839e8486
51 ändrade filer med 238 tillägg och 54 borttagningar
  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):