Add WS command number/device_class_convertible_units (#85598)

* Add WS command number/device_class_convertible_units

* Add websocket_api

* Update tests
This commit is contained in:
Erik Montnemery 2023-01-12 09:34:10 +01:00 committed by GitHub
parent 305fb86d50
commit a7fb3c82fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 491 additions and 297 deletions

View File

@ -24,7 +24,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter
from .const import (
ATTR_MAX,
@ -36,7 +35,10 @@ from .const import (
DEFAULT_STEP,
DOMAIN,
SERVICE_SET_VALUE,
UNIT_CONVERTERS,
NumberDeviceClass,
)
from .websocket_api import async_setup as async_setup_ws_api
SCAN_INTERVAL = timedelta(seconds=30)
@ -46,297 +48,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
class NumberDeviceClass(StrEnum):
"""Device class for numbers."""
# NumberDeviceClass should be aligned with SensorDeviceClass
APPARENT_POWER = "apparent_power"
"""Apparent power.
Unit of measurement: `VA`
"""
AQI = "aqi"
"""Air Quality Index.
Unit of measurement: `None`
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
"""Atmospheric pressure.
Unit of measurement: `UnitOfPressure` units
"""
BATTERY = "battery"
"""Percentage of battery that is left.
Unit of measurement: `%`
"""
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppm` (parts per million)
"""
CO2 = "carbon_dioxide"
"""Carbon Dioxide gas concentration.
Unit of measurement: `ppm` (parts per million)
"""
CURRENT = "current"
"""Current.
Unit of measurement: `A`, `mA`
"""
DATA_RATE = "data_rate"
"""Data rate.
Unit of measurement: UnitOfDataRate
"""
DATA_SIZE = "data_size"
"""Data size.
Unit of measurement: UnitOfInformation
"""
DISTANCE = "distance"
"""Generic distance.
Unit of measurement: `LENGTH_*` units
- SI /metric: `mm`, `cm`, `m`, `km`
- USCS / imperial: `in`, `ft`, `yd`, `mi`
"""
ENERGY = "energy"
"""Energy.
Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ`
"""
FREQUENCY = "frequency"
"""Frequency.
Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz`
"""
GAS = "gas"
"""Gas.
Unit of measurement:
- SI / metric: ``
- USCS / imperial: `ft³`, `CCF`
"""
HUMIDITY = "humidity"
"""Relative humidity.
Unit of measurement: `%`
"""
ILLUMINANCE = "illuminance"
"""Illuminance.
Unit of measurement: `lx`
"""
IRRADIANCE = "irradiance"
"""Irradiance.
Unit of measurement:
- SI / metric: `W/`
- USCS / imperial: `BTU/(hft²)`
"""
MOISTURE = "moisture"
"""Moisture.
Unit of measurement: `%`
"""
MONETARY = "monetary"
"""Amount of money.
Unit of measurement: ISO4217 currency code
See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes
"""
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `µg/`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO.
Unit of measurement: `µg/`
"""
NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O.
Unit of measurement: `µg/`
"""
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `µg/`
"""
PM1 = "pm1"
"""Particulate matter <= 0.1 μm.
Unit of measurement: `µg/`
"""
PM10 = "pm10"
"""Particulate matter <= 10 μm.
Unit of measurement: `µg/`
"""
PM25 = "pm25"
"""Particulate matter <= 2.5 μm.
Unit of measurement: `µg/`
"""
POWER_FACTOR = "power_factor"
"""Power factor.
Unit of measurement: `%`, `None`
"""
POWER = "power"
"""Power.
Unit of measurement: `W`, `kW`
"""
PRECIPITATION = "precipitation"
"""Accumulated precipitation.
Unit of measurement: UnitOfPrecipitationDepth
- SI / metric: `cm`, `mm`
- USCS / imperial: `in`
"""
PRECIPITATION_INTENSITY = "precipitation_intensity"
"""Precipitation intensity.
Unit of measurement: UnitOfVolumetricFlux
- SI /metric: `mm/d`, `mm/h`
- USCS / imperial: `in/d`, `in/h`
"""
PRESSURE = "pressure"
"""Pressure.
Unit of measurement:
- `mbar`, `cbar`, `bar`
- `Pa`, `hPa`, `kPa`
- `inHg`
- `psi`
"""
REACTIVE_POWER = "reactive_power"
"""Reactive power.
Unit of measurement: `var`
"""
SIGNAL_STRENGTH = "signal_strength"
"""Signal strength.
Unit of measurement: `dB`, `dBm`
"""
SOUND_PRESSURE = "sound_pressure"
"""Sound pressure.
Unit of measurement: `dB`, `dBA`
"""
SPEED = "speed"
"""Generic speed.
Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux`
- SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`
- USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph`
- Nautical: `kn`
"""
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `µg/`
"""
TEMPERATURE = "temperature"
"""Temperature.
Unit of measurement: `°C`, `°F`
"""
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""Amount of VOC.
Unit of measurement: `µg/`
"""
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`, `mV`
"""
VOLUME = "volume"
"""Generic volume.
Unit of measurement: `VOLUME_*` units
- SI / metric: `mL`, `L`, ``
- USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
WATER = "water"
"""Water.
Unit of measurement:
- SI / metric: ``, `L`
- USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
WEIGHT = "weight"
"""Generic weight, represents a measurement of an object's mass.
Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units
- SI / metric: `µg`, `mg`, `g`, `kg`
- USCS / imperial: `oz`, `lb`
"""
WIND_SPEED = "wind_speed"
"""Wind speed.
Unit of measurement: `SPEED_*` units
- SI /metric: `m/s`, `km/h`
- USCS / imperial: `ft/s`, `mph`
- Nautical: `kn`
"""
DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass))
@ -348,10 +59,6 @@ class NumberMode(StrEnum):
SLIDER = "slider"
UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
}
# mypy: disallow-any-generics
@ -360,6 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component = hass.data[DOMAIN] = EntityComponent[NumberEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
async_setup_ws_api(hass)
await component.async_setup(config)
component.async_register_entity_service(

View File

@ -1,7 +1,38 @@
"""Provides the constants needed for the component."""
from __future__ import annotations
from typing import Final
from homeassistant.backports.enum import StrEnum
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
POWER_VOLT_AMPERE_REACTIVE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
UnitOfDataRate,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfInformation,
UnitOfIrradiance,
UnitOfLength,
UnitOfMass,
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolume,
UnitOfVolumetricFlux,
)
from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter
ATTR_VALUE = "value"
ATTR_MIN = "min"
ATTR_MAX = "max"
@ -19,3 +50,360 @@ SERVICE_SET_VALUE = "set_value"
MODE_AUTO: Final = "auto"
MODE_BOX: Final = "box"
MODE_SLIDER: Final = "slider"
class NumberDeviceClass(StrEnum):
"""Device class for numbers."""
# NumberDeviceClass should be aligned with NumberDeviceClass
APPARENT_POWER = "apparent_power"
"""Apparent power.
Unit of measurement: `VA`
"""
AQI = "aqi"
"""Air Quality Index.
Unit of measurement: `None`
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
"""Atmospheric pressure.
Unit of measurement: `UnitOfPressure` units
"""
BATTERY = "battery"
"""Percentage of battery that is left.
Unit of measurement: `%`
"""
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppm` (parts per million)
"""
CO2 = "carbon_dioxide"
"""Carbon Dioxide gas concentration.
Unit of measurement: `ppm` (parts per million)
"""
CURRENT = "current"
"""Current.
Unit of measurement: `A`, `mA`
"""
DATA_RATE = "data_rate"
"""Data rate.
Unit of measurement: UnitOfDataRate
"""
DATA_SIZE = "data_size"
"""Data size.
Unit of measurement: UnitOfInformation
"""
DISTANCE = "distance"
"""Generic distance.
Unit of measurement: `LENGTH_*` units
- SI /metric: `mm`, `cm`, `m`, `km`
- USCS / imperial: `in`, `ft`, `yd`, `mi`
"""
ENERGY = "energy"
"""Energy.
Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ`
"""
FREQUENCY = "frequency"
"""Frequency.
Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz`
"""
GAS = "gas"
"""Gas.
Unit of measurement:
- SI / metric: ``
- USCS / imperial: `ft³`, `CCF`
"""
HUMIDITY = "humidity"
"""Relative humidity.
Unit of measurement: `%`
"""
ILLUMINANCE = "illuminance"
"""Illuminance.
Unit of measurement: `lx`
"""
IRRADIANCE = "irradiance"
"""Irradiance.
Unit of measurement:
- SI / metric: `W/`
- USCS / imperial: `BTU/(hft²)`
"""
MOISTURE = "moisture"
"""Moisture.
Unit of measurement: `%`
"""
MONETARY = "monetary"
"""Amount of money.
Unit of measurement: ISO4217 currency code
See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes
"""
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `µg/`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO.
Unit of measurement: `µg/`
"""
NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O.
Unit of measurement: `µg/`
"""
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `µg/`
"""
PM1 = "pm1"
"""Particulate matter <= 0.1 μm.
Unit of measurement: `µg/`
"""
PM10 = "pm10"
"""Particulate matter <= 10 μm.
Unit of measurement: `µg/`
"""
PM25 = "pm25"
"""Particulate matter <= 2.5 μm.
Unit of measurement: `µg/`
"""
POWER_FACTOR = "power_factor"
"""Power factor.
Unit of measurement: `%`, `None`
"""
POWER = "power"
"""Power.
Unit of measurement: `W`, `kW`
"""
PRECIPITATION = "precipitation"
"""Accumulated precipitation.
Unit of measurement: UnitOfPrecipitationDepth
- SI / metric: `cm`, `mm`
- USCS / imperial: `in`
"""
PRECIPITATION_INTENSITY = "precipitation_intensity"
"""Precipitation intensity.
Unit of measurement: UnitOfVolumetricFlux
- SI /metric: `mm/d`, `mm/h`
- USCS / imperial: `in/d`, `in/h`
"""
PRESSURE = "pressure"
"""Pressure.
Unit of measurement:
- `mbar`, `cbar`, `bar`
- `Pa`, `hPa`, `kPa`
- `inHg`
- `psi`
"""
REACTIVE_POWER = "reactive_power"
"""Reactive power.
Unit of measurement: `var`
"""
SIGNAL_STRENGTH = "signal_strength"
"""Signal strength.
Unit of measurement: `dB`, `dBm`
"""
SOUND_PRESSURE = "sound_pressure"
"""Sound pressure.
Unit of measurement: `dB`, `dBA`
"""
SPEED = "speed"
"""Generic speed.
Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux`
- SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`
- USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph`
- Nautical: `kn`
"""
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `µg/`
"""
TEMPERATURE = "temperature"
"""Temperature.
Unit of measurement: `°C`, `°F`
"""
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""Amount of VOC.
Unit of measurement: `µg/`
"""
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`, `mV`
"""
VOLUME = "volume"
"""Generic volume.
Unit of measurement: `VOLUME_*` units
- SI / metric: `mL`, `L`, ``
- USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
WATER = "water"
"""Water.
Unit of measurement:
- SI / metric: ``, `L`
- USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in
USCS/imperial units are currently assumed to be US volumes)
"""
WEIGHT = "weight"
"""Generic weight, represents a measurement of an object's mass.
Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units
- SI / metric: `µg`, `mg`, `g`, `kg`
- USCS / imperial: `oz`, `lb`
"""
WIND_SPEED = "wind_speed"
"""Wind speed.
Unit of measurement: `SPEED_*` units
- SI /metric: `m/s`, `km/h`
- USCS / imperial: `ft/s`, `mph`
- Nautical: `kn`
"""
DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
NumberDeviceClass.AQI: {None},
NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
NumberDeviceClass.BATTERY: {PERCENTAGE},
NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION},
NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
NumberDeviceClass.CURRENT: set(UnitOfElectricCurrent),
NumberDeviceClass.DATA_RATE: set(UnitOfDataRate),
NumberDeviceClass.DATA_SIZE: set(UnitOfInformation),
NumberDeviceClass.DISTANCE: set(UnitOfLength),
NumberDeviceClass.ENERGY: set(UnitOfEnergy),
NumberDeviceClass.FREQUENCY: set(UnitOfFrequency),
NumberDeviceClass.GAS: {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
},
NumberDeviceClass.HUMIDITY: {PERCENTAGE},
NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX},
NumberDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
NumberDeviceClass.MOISTURE: {PERCENTAGE},
NumberDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
NumberDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT},
NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
NumberDeviceClass.PRESSURE: set(UnitOfPressure),
NumberDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE},
NumberDeviceClass.SIGNAL_STRENGTH: {
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
},
NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
NumberDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)),
NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.TEMPERATURE: {
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
},
NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
},
NumberDeviceClass.VOLTAGE: set(UnitOfElectricPotential),
NumberDeviceClass.VOLUME: set(UnitOfVolume),
NumberDeviceClass.WATER: {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.GALLONS,
UnitOfVolume.LITERS,
},
NumberDeviceClass.WEIGHT: set(UnitOfMass),
NumberDeviceClass.WIND_SPEED: set(UnitOfSpeed),
}
UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
}

View File

@ -0,0 +1,35 @@
"""The sensor websocket API."""
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from .const import DEVICE_CLASS_UNITS, UNIT_CONVERTERS
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the number websocket API."""
websocket_api.async_register_command(hass, ws_device_class_units)
@callback
@websocket_api.websocket_command(
{
vol.Required("type"): "number/device_class_convertible_units",
vol.Required("device_class"): str,
}
)
def ws_device_class_units(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Return supported units for a device class."""
device_class = msg["device_class"]
convertible_units = set()
if device_class in UNIT_CONVERTERS and device_class in DEVICE_CLASS_UNITS:
convertible_units = DEVICE_CLASS_UNITS[device_class]
connection.send_result(msg["id"], {"units": convertible_units})

View File

@ -14,7 +14,13 @@ from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.number.const import (
DEVICE_CLASS_UNITS as NUMBER_DEVICE_CLASS_UNITS,
)
from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS as SENSOR_DEVICE_CLASS_UNITS,
SensorDeviceClass,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
@ -867,3 +873,11 @@ def test_device_classes_aligned():
assert hasattr(NumberDeviceClass, device_class.name)
assert getattr(NumberDeviceClass, device_class.name).value == device_class.value
for device_class in SENSOR_DEVICE_CLASS_UNITS:
if device_class in non_numeric_device_classes:
continue
assert (
SENSOR_DEVICE_CLASS_UNITS[device_class]
== NUMBER_DEVICE_CLASS_UNITS[device_class]
)

View File

@ -0,0 +1,49 @@
"""Test the number websocket API."""
from pytest_unordered import unordered
from homeassistant.components.number.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None:
"""Test we can get supported units."""
assert await async_setup_component(hass, DOMAIN, {})
client = await hass_ws_client(hass)
# Device class with units which number allows customizing & converting
await client.send_json(
{
"id": 1,
"type": "number/device_class_convertible_units",
"device_class": "temperature",
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"units": unordered(["°F", "°C"])}
# Device class with units which number doesn't allow customizing & converting
await client.send_json(
{
"id": 2,
"type": "number/device_class_convertible_units",
"device_class": "energy",
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"units": []}
# Unknown device class
await client.send_json(
{
"id": 3,
"type": "number/device_class_convertible_units",
"device_class": "kebabsås",
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {"units": unordered([])}