-
-
Notifications
You must be signed in to change notification settings - Fork 37.6k
Add sensor unit of measurement validation for device classes #84366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ | |
| from homeassistant.backports.enum import StrEnum | ||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecated-import] | ||
| CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, | ||
| CONCENTRATION_PARTS_PER_MILLION, | ||
| CONF_UNIT_OF_MEASUREMENT, | ||
| DEVICE_CLASS_AQI, | ||
| DEVICE_CLASS_BATTERY, | ||
|
|
@@ -45,7 +47,30 @@ | |
| DEVICE_CLASS_TIMESTAMP, | ||
| DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||
| DEVICE_CLASS_VOLTAGE, | ||
| 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, | ||
| UnitOfTime, | ||
| UnitOfVolume, | ||
| UnitOfVolumetricFlux, | ||
| ) | ||
| from homeassistant.core import HomeAssistant, callback | ||
| from homeassistant.helpers import entity_registry as er | ||
|
|
@@ -453,6 +478,74 @@ class SensorStateClass(StrEnum): | |
| SensorDeviceClass.WIND_SPEED: SpeedConverter, | ||
| } | ||
|
|
||
| DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { | ||
| SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), | ||
| SensorDeviceClass.AQI: {None}, | ||
| SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), | ||
| SensorDeviceClass.BATTERY: {PERCENTAGE}, | ||
| SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, | ||
| SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, | ||
| SensorDeviceClass.CURRENT: {UnitOfElectricCurrent.AMPERE}, | ||
| SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), | ||
| SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), | ||
| SensorDeviceClass.DISTANCE: set(UnitOfLength), | ||
| SensorDeviceClass.DURATION: { | ||
| UnitOfTime.DAYS, | ||
| UnitOfTime.HOURS, | ||
| UnitOfTime.MINUTES, | ||
| UnitOfTime.SECONDS, | ||
| }, | ||
| SensorDeviceClass.ENERGY: set(UnitOfEnergy), | ||
| SensorDeviceClass.FREQUENCY: set(UnitOfFrequency), | ||
| SensorDeviceClass.GAS: { | ||
| UnitOfVolume.CENTUM_CUBIC_FEET, | ||
| UnitOfVolume.CUBIC_FEET, | ||
| UnitOfVolume.CUBIC_METERS, | ||
| }, | ||
| SensorDeviceClass.HUMIDITY: {PERCENTAGE}, | ||
| SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX, "lm"}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if I have opened an architecture discussion about it in home-assistant/architecture#843
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR is not adding something new, the discussion is fine, but it doesn't have to block this. |
||
| SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance), | ||
| SensorDeviceClass.MOISTURE: {PERCENTAGE}, | ||
| SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.POWER_FACTOR: {PERCENTAGE}, | ||
| SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, | ||
| SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), | ||
| SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), | ||
| SensorDeviceClass.PRESSURE: set(UnitOfPressure), | ||
| SensorDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, | ||
| SensorDeviceClass.SIGNAL_STRENGTH: { | ||
| SIGNAL_STRENGTH_DECIBELS, | ||
| SIGNAL_STRENGTH_DECIBELS_MILLIWATT, | ||
| }, | ||
| SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), | ||
| SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), | ||
| SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, | ||
| SensorDeviceClass.TEMPERATURE: { | ||
| UnitOfTemperature.CELSIUS, | ||
| UnitOfTemperature.FAHRENHEIT, | ||
| }, | ||
| SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { | ||
| CONCENTRATION_MICROGRAMS_PER_CUBIC_METER | ||
| }, | ||
| SensorDeviceClass.VOLTAGE: {UnitOfElectricPotential.VOLT}, | ||
| SensorDeviceClass.VOLUME: set(UnitOfVolume), | ||
| SensorDeviceClass.WATER: { | ||
| UnitOfVolume.CENTUM_CUBIC_FEET, | ||
| UnitOfVolume.CUBIC_FEET, | ||
| UnitOfVolume.CUBIC_METERS, | ||
| UnitOfVolume.GALLONS, | ||
| UnitOfVolume.LITERS, | ||
| }, | ||
| SensorDeviceClass.WEIGHT: set(UnitOfMass), | ||
| SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), | ||
| } | ||
|
|
||
| # mypy: disallow-any-generics | ||
|
|
||
|
|
||
|
|
@@ -506,6 +599,7 @@ class SensorEntity(Entity): | |
| _attr_unit_of_measurement: None = ( | ||
| None # Subclasses of SensorEntity should not set this | ||
| ) | ||
| _invalid_unit_of_measurement_reported = False | ||
| _last_reset_reported = False | ||
| _sensor_option_unit_of_measurement: str | None = None | ||
|
|
||
|
|
@@ -862,6 +956,29 @@ def state(self) -> Any: | |
| # Round to the wanted precision | ||
| value = round(value_f_new) if prec == 0 else round(value_f_new, prec) | ||
|
|
||
| # Validate unit of measurement used for sensors with a device class | ||
| if ( | ||
| not self._invalid_unit_of_measurement_reported | ||
| and device_class | ||
| and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None | ||
| and native_unit_of_measurement not in units | ||
| ): | ||
| self._invalid_unit_of_measurement_reported = True | ||
| report_issue = self._suggest_report_issue() | ||
|
|
||
| # This should raise in Home Assistant Core 2023.6 | ||
| _LOGGER.warning( | ||
| "Entity %s (%s) is using native unit of measurement '%s' which " | ||
| "is not a valid unit for the device class ('%s') it is using; " | ||
| "Please update your configuration if your entity is manually " | ||
| "configured, otherwise %s", | ||
| self.entity_id, | ||
| type(self), | ||
| native_unit_of_measurement, | ||
| device_class, | ||
| report_issue, | ||
| ) | ||
|
|
||
| return value | ||
|
|
||
| def __repr__(self) -> str: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to have the same validation for number entities?
And would it make sense to have this dict shared somewhere?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have no validations on numbers whatsoever at this point. But yes, we can add more validation there too.
I suggest doing that at a later point.
Essentially, those are not the same device classes... (we can enforce keeping them in sync though)