Use greek small letter mu "\u03bc" instead of micro sign "\u00B5" for micro unit prefix (alt 1) (#144853)

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Jan Bouwhuis
2025-08-19 18:48:50 +02:00
committed by GitHub
parent 48091e5995
commit 48300f4563
52 changed files with 936 additions and 226 deletions

View File

@@ -61,7 +61,7 @@
"display_pm_standard": { "display_pm_standard": {
"name": "Display PM standard", "name": "Display PM standard",
"state": { "state": {
"ugm3": "µg/m³", "ugm3": "μg/m³",
"us_aqi": "US AQI" "us_aqi": "US AQI"
} }
}, },

View File

@@ -72,7 +72,7 @@ SENSOR_DESCRIPTIONS = {
key=str(BTHomeExtendedSensorDeviceClass.CHANNEL), key=str(BTHomeExtendedSensorDeviceClass.CHANNEL),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# Conductivity (µS/cm) # Conductivity (μS/cm)
( (
BTHomeSensorDeviceClass.CONDUCTIVITY, BTHomeSensorDeviceClass.CONDUCTIVITY,
Units.CONDUCTIVITY, Units.CONDUCTIVITY,
@@ -215,7 +215,7 @@ SENSOR_DESCRIPTIONS = {
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
# PM10 (µg/m3) # PM10 (μg/m3)
( (
BTHomeSensorDeviceClass.PM10, BTHomeSensorDeviceClass.PM10,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -225,7 +225,7 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# PM2.5 (µg/m3) # PM2.5 (μg/m3)
( (
BTHomeSensorDeviceClass.PM25, BTHomeSensorDeviceClass.PM25,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -318,7 +318,7 @@ SENSOR_DESCRIPTIONS = {
key=str(BTHomeSensorDeviceClass.UV_INDEX), key=str(BTHomeSensorDeviceClass.UV_INDEX),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# Volatile organic Compounds (VOC) (µg/m3) # Volatile organic Compounds (VOC) (μg/m3)
( (
BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,

View File

@@ -99,6 +99,18 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry, version=1, minor_version=3 config_entry, version=1, minor_version=3
) )
if config_entry.minor_version < 4:
# Ensure we use the correct units
new_options = {**config_entry.options}
if new_options.get("unit_prefix") == "\u00b5":
# Ensure we use the preferred coding of μ
new_options["unit_prefix"] = "\u03bc"
hass.config_entries.async_update_entry(
config_entry, options=new_options, version=1, minor_version=4
)
_LOGGER.debug( _LOGGER.debug(
"Migration to configuration version %s.%s successful", "Migration to configuration version %s.%s successful",
config_entry.version, config_entry.version,

View File

@@ -36,7 +36,7 @@ from .const import (
UNIT_PREFIXES = [ UNIT_PREFIXES = [
selector.SelectOptionDict(value="n", label="n (nano)"), selector.SelectOptionDict(value="n", label="n (nano)"),
selector.SelectOptionDict(value="µ", label="µ (micro)"), selector.SelectOptionDict(value="μ", label="μ (micro)"),
selector.SelectOptionDict(value="m", label="m (milli)"), selector.SelectOptionDict(value="m", label="m (milli)"),
selector.SelectOptionDict(value="k", label="k (kilo)"), selector.SelectOptionDict(value="k", label="k (kilo)"),
selector.SelectOptionDict(value="M", label="M (mega)"), selector.SelectOptionDict(value="M", label="M (mega)"),
@@ -142,7 +142,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
options_flow = OPTIONS_FLOW options_flow = OPTIONS_FLOW
VERSION = 1 VERSION = 1
MINOR_VERSION = 3 MINOR_VERSION = 4
def async_config_entry_title(self, options: Mapping[str, Any]) -> str: def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title.""" """Return config entry title."""

View File

@@ -63,7 +63,7 @@ ATTR_SOURCE_ID = "source"
UNIT_PREFIXES = { UNIT_PREFIXES = {
None: 1, None: 1,
"n": 1e-9, "n": 1e-9,
"µ": 1e-6, "μ": 1e-6,
"m": 1e-3, "m": 1e-3,
"k": 1e3, "k": 1e3,
"M": 1e6, "M": 1e6,

View File

@@ -157,7 +157,7 @@ SENSORS: dict[str | None, SensorEntityDescription] = {
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
"µg/m³": SensorEntityDescription( "μg/m³": SensorEntityDescription(
key="concentration|microgram_per_cubic_meter", key="concentration|microgram_per_cubic_meter",
translation_key="concentration", translation_key="concentration",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,

View File

@@ -291,7 +291,7 @@ class NitrogenDioxideSensor(AirQualitySensor):
class VolatileOrganicCompoundsSensor(AirQualitySensor): class VolatileOrganicCompoundsSensor(AirQualitySensor):
"""Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor. """Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor.
Sensor entity must return VOC in µg/m3. Sensor entity must return VOC in μg/m3.
""" """
def create_services(self) -> None: def create_services(self) -> None:

View File

@@ -494,7 +494,7 @@ def temperature_to_states(temperature: float, unit: str) -> float:
def density_to_air_quality(density: float) -> int: def density_to_air_quality(density: float) -> int:
"""Map PM2.5 µg/m3 density to HomeKit AirQuality level.""" """Map PM2.5 μg/m3 density to HomeKit AirQuality level."""
if density <= 9: # US AQI 0-50 (HomeKit: Excellent) if density <= 9: # US AQI 0-50 (HomeKit: Excellent)
return 1 return 1
if density <= 35.4: # US AQI 51-100 (HomeKit: Good) if density <= 35.4: # US AQI 51-100 (HomeKit: Good)
@@ -507,7 +507,7 @@ def density_to_air_quality(density: float) -> int:
def density_to_air_quality_pm10(density: float) -> int: def density_to_air_quality_pm10(density: float) -> int:
"""Map PM10 µg/m3 density to HomeKit AirQuality level.""" """Map PM10 μg/m3 density to HomeKit AirQuality level."""
if density <= 54: # US AQI 0-50 (HomeKit: Excellent) if density <= 54: # US AQI 0-50 (HomeKit: Excellent)
return 1 return 1
if density <= 154: # US AQI 51-100 (HomeKit: Good) if density <= 154: # US AQI 51-100 (HomeKit: Good)
@@ -520,7 +520,7 @@ def density_to_air_quality_pm10(density: float) -> int:
def density_to_air_quality_nitrogen_dioxide(density: float) -> int: def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
"""Map nitrogen dioxide µg/m3 to HomeKit AirQuality level.""" """Map nitrogen dioxide μg/m3 to HomeKit AirQuality level."""
if density <= 30: if density <= 30:
return 1 return 1
if density <= 60: if density <= 60:
@@ -533,7 +533,7 @@ def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
def density_to_air_quality_voc(density: float) -> int: def density_to_air_quality_voc(density: float) -> int:
"""Map VOCs µg/m3 to HomeKit AirQuality level. """Map VOCs μg/m3 to HomeKit AirQuality level.
The VOC mappings use the IAQ guidelines for Europe released by the WHO (World Health Organization). The VOC mappings use the IAQ guidelines for Europe released by the WHO (World Health Organization).
Referenced from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf Referenced from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf

View File

@@ -16,6 +16,7 @@ from homeassistant.components.number import (
NumberMode, NumberMode,
RestoreNumber, RestoreNumber,
) )
from homeassistant.components.sensor import AMBIGUOUS_UNITS
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
@@ -70,6 +71,12 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
def validate_config(config: ConfigType) -> ConfigType: def validate_config(config: ConfigType) -> ConfigType:
"""Validate that the configuration is valid, throws if it isn't.""" """Validate that the configuration is valid, throws if it isn't."""
if (
CONF_UNIT_OF_MEASUREMENT in config
and (unit_of_measurement := config[CONF_UNIT_OF_MEASUREMENT]) in AMBIGUOUS_UNITS
):
config[CONF_UNIT_OF_MEASUREMENT] = AMBIGUOUS_UNITS[unit_of_measurement]
if config[CONF_MIN] > config[CONF_MAX]: if config[CONF_MIN] > config[CONF_MAX]:
raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}") raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}")

View File

@@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant.components import sensor from homeassistant.components import sensor
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
AMBIGUOUS_UNITS,
CONF_STATE_CLASS, CONF_STATE_CLASS,
DEVICE_CLASS_UNITS, DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA, DEVICE_CLASSES_SCHEMA,
@@ -133,9 +134,14 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"together with state class '{state_class}'" f"together with state class '{state_class}'"
) )
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or ( if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None:
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) return config
) is None:
unit_of_measurement = config[CONF_UNIT_OF_MEASUREMENT] = AMBIGUOUS_UNITS.get(
unit_of_measurement, unit_of_measurement
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None:
return config return config
if ( if (

View File

@@ -31,6 +31,7 @@ from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401 from .const import ( # noqa: F401
AMBIGUOUS_UNITS,
ATTR_MAX, ATTR_MAX,
ATTR_MIN, ATTR_MIN,
ATTR_STEP, ATTR_STEP,
@@ -368,6 +369,15 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self.entity_description.native_unit_of_measurement return self.entity_description.native_unit_of_measurement
return None return None
@final
@property
def __native_unit_of_measurement_compat(self) -> str | None:
"""Process ambiguous units."""
native_unit_of_measurement = self.native_unit_of_measurement
return AMBIGUOUS_UNITS.get(
native_unit_of_measurement, native_unit_of_measurement
)
@property @property
@final @final
def unit_of_measurement(self) -> str | None: def unit_of_measurement(self) -> str | None:
@@ -375,7 +385,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if self._number_option_unit_of_measurement: if self._number_option_unit_of_measurement:
return self._number_option_unit_of_measurement return self._number_option_unit_of_measurement
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
# device_class is checked after native_unit_of_measurement since most # device_class is checked after native_unit_of_measurement since most
# of the time we can avoid the device_class check # of the time we can avoid the device_class check
if ( if (
@@ -444,7 +454,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if device_class not in UNIT_CONVERTERS: if device_class not in UNIT_CONVERTERS:
return value return value
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
unit_of_measurement = self.unit_of_measurement unit_of_measurement = self.unit_of_measurement
if native_unit_of_measurement != unit_of_measurement: if native_unit_of_measurement != unit_of_measurement:
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -473,7 +483,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS: if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS:
return value return value
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
unit_of_measurement = self.unit_of_measurement unit_of_measurement = self.unit_of_measurement
if native_unit_of_measurement != unit_of_measurement: if native_unit_of_measurement != unit_of_measurement:
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -496,7 +506,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
(number_options := self.registry_entry.options.get(DOMAIN)) (number_options := self.registry_entry.options.get(DOMAIN))
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
and (device_class := self.device_class) in UNIT_CONVERTERS and (device_class := self.device_class) in UNIT_CONVERTERS
and self.native_unit_of_measurement and self.__native_unit_of_measurement_compat
in UNIT_CONVERTERS[device_class].VALID_UNITS in UNIT_CONVERTERS[device_class].VALID_UNITS
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
): ):

View File

@@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER, CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@@ -136,7 +137,7 @@ class NumberDeviceClass(StrEnum):
CONDUCTIVITY = "conductivity" CONDUCTIVITY = "conductivity"
"""Conductivity. """Conductivity.
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm` Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
""" """
CURRENT = "current" CURRENT = "current"
@@ -168,7 +169,7 @@ class NumberDeviceClass(StrEnum):
DURATION = "duration" DURATION = "duration"
"""Fixed duration. """Fixed duration.
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs` Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
""" """
ENERGY = "energy" ENERGY = "energy"
@@ -246,25 +247,25 @@ class NumberDeviceClass(StrEnum):
NITROGEN_DIOXIDE = "nitrogen_dioxide" NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2. """Amount of NO2.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
NITROGEN_MONOXIDE = "nitrogen_monoxide" NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO. """Amount of NO.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
NITROUS_OXIDE = "nitrous_oxide" NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O. """Amount of N2O.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
OZONE = "ozone" OZONE = "ozone"
"""Amount of O3. """Amount of O3.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
PH = "ph" PH = "ph"
@@ -276,19 +277,19 @@ class NumberDeviceClass(StrEnum):
PM1 = "pm1" PM1 = "pm1"
"""Particulate matter <= 1 μm. """Particulate matter <= 1 μm.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
PM10 = "pm10" PM10 = "pm10"
"""Particulate matter <= 10 μm. """Particulate matter <= 10 μm.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
PM25 = "pm25" PM25 = "pm25"
"""Particulate matter <= 2.5 μm. """Particulate matter <= 2.5 μm.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
POWER_FACTOR = "power_factor" POWER_FACTOR = "power_factor"
@@ -365,7 +366,7 @@ class NumberDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide" SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2. """Amount of SO2.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
TEMPERATURE = "temperature" TEMPERATURE = "temperature"
@@ -377,7 +378,7 @@ class NumberDeviceClass(StrEnum):
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""Amount of VOC. """Amount of VOC.
Unit of measurement: `µg/m³`, `mg/m³` Unit of measurement: `μg/m³`, `mg/m³`
""" """
VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
@@ -389,7 +390,7 @@ class NumberDeviceClass(StrEnum):
VOLTAGE = "voltage" VOLTAGE = "voltage"
"""Voltage. """Voltage.
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV` Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
""" """
VOLUME = "volume" VOLUME = "volume"
@@ -436,7 +437,7 @@ class NumberDeviceClass(StrEnum):
Weight is used instead of mass to fit with every day language. Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units Unit of measurement: `MASS_*` units
- SI / metric: `µg`, `mg`, `g`, `kg` - SI / metric: `μg`, `mg`, `g`, `kg`
- USCS / imperial: `oz`, `lb` - USCS / imperial: `oz`, `lb`
""" """
@@ -556,3 +557,16 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
NumberDeviceClass.TEMPERATURE: TemperatureConverter, NumberDeviceClass.TEMPERATURE: TemperatureConverter,
NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter,
} }
# We translate units that were using using the legacy coding of μ \u00b5
# to units using recommended coding of μ \u03bc
AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
}

View File

@@ -366,6 +366,7 @@ class PrometheusMetrics:
@staticmethod @staticmethod
def _sanitize_metric_name(metric: str) -> str: def _sanitize_metric_name(metric: str) -> str:
metric.replace("\u03bc", "\u00b5")
return "".join( return "".join(
[c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric] [c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric]
) )
@@ -747,6 +748,9 @@ class PrometheusMetrics:
PERCENTAGE: "percent", PERCENTAGE: "percent",
} }
default = unit.replace("/", "_per_") default = unit.replace("/", "_per_")
# Unit conversion for CONCENTRATION_MICROGRAMS_PER_CUBIC_METER "μg/m³"
# "μ" == "\u03bc" but the API uses "\u00b5"
default = default.replace("\u03bc", "\u00b5")
default = default.lower() default = default.lower()
return units.get(unit, default) return units.get(unit, default)

View File

@@ -261,7 +261,7 @@ def correct_db_schema_precision(
from ..migration import _modify_columns # noqa: PLC0415 from ..migration import _modify_columns # noqa: PLC0415
precision_columns = _get_precision_column_types(table_object) precision_columns = _get_precision_column_types(table_object)
# Attempt to convert timestamp columns to µs precision # Attempt to convert timestamp columns to μs precision
session_maker = instance.get_session session_maker = instance.get_session
engine = instance.engine engine = instance.engine
assert engine is not None, "Engine should be set" assert engine is not None, "Engine should be set"

View File

@@ -34,6 +34,7 @@ from homeassistant.util.enum import try_parse_enum
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401 from .const import ( # noqa: F401
AMBIGUOUS_UNITS,
ATTR_LAST_RESET, ATTR_LAST_RESET,
ATTR_OPTIONS, ATTR_OPTIONS,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
@@ -314,7 +315,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return _numeric_state_expected( return _numeric_state_expected(
try_parse_enum(SensorDeviceClass, self.device_class), try_parse_enum(SensorDeviceClass, self.device_class),
self.state_class, self.state_class,
self.native_unit_of_measurement, self.__native_unit_of_measurement_compat,
self.suggested_display_precision, self.suggested_display_precision,
) )
@@ -366,7 +367,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Make sure we can convert the units # Make sure we can convert the units
if ( if (
(unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None (unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None
or self.native_unit_of_measurement not in unit_converter.VALID_UNITS or self.__native_unit_of_measurement_compat
not in unit_converter.VALID_UNITS
or suggested_unit_of_measurement not in unit_converter.VALID_UNITS or suggested_unit_of_measurement not in unit_converter.VALID_UNITS
): ):
if not self._invalid_suggested_unit_of_measurement_reported: if not self._invalid_suggested_unit_of_measurement_reported:
@@ -387,7 +389,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if suggested_unit_of_measurement is None: if suggested_unit_of_measurement is None:
# Fallback to unit suggested by the unit conversion rules from device class # Fallback to unit suggested by the unit conversion rules from device class
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit( suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
self.device_class, self.native_unit_of_measurement self.device_class, self.__native_unit_of_measurement_compat
) )
if suggested_unit_of_measurement is None and ( if suggested_unit_of_measurement is None and (
@@ -396,7 +398,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# If the device class is not known by the unit system but has a unit converter, # If the device class is not known by the unit system but has a unit converter,
# fall back to the unit suggested by the unit converter's unit class. # fall back to the unit suggested by the unit converter's unit class.
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit( suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
unit_converter.UNIT_CLASS, self.native_unit_of_measurement unit_converter.UNIT_CLASS, self.__native_unit_of_measurement_compat
) )
if suggested_unit_of_measurement is None: if suggested_unit_of_measurement is None:
@@ -468,6 +470,16 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self.entity_description.native_unit_of_measurement return self.entity_description.native_unit_of_measurement
return None return None
@final
@property
def __native_unit_of_measurement_compat(self) -> str | None:
"""Process ambiguous units."""
native_unit_of_measurement = self.native_unit_of_measurement
return AMBIGUOUS_UNITS.get(
native_unit_of_measurement,
native_unit_of_measurement,
)
@cached_property @cached_property
def suggested_unit_of_measurement(self) -> str | None: def suggested_unit_of_measurement(self) -> str | None:
"""Return the unit which should be used for the sensor's state. """Return the unit which should be used for the sensor's state.
@@ -503,7 +515,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if self._sensor_option_unit_of_measurement is not UNDEFINED: if self._sensor_option_unit_of_measurement is not UNDEFINED:
return self._sensor_option_unit_of_measurement return self._sensor_option_unit_of_measurement
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
# Second priority, for non registered entities: unit suggested by integration # Second priority, for non registered entities: unit suggested by integration
if not self.registry_entry and ( if not self.registry_entry and (
@@ -543,7 +555,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@override @override
def state(self) -> Any: def state(self) -> Any:
"""Return the state of the sensor and perform unit conversions, if needed.""" """Return the state of the sensor and perform unit conversions, if needed."""
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
unit_of_measurement = self.unit_of_measurement unit_of_measurement = self.unit_of_measurement
value = self.native_value value = self.native_value
# For the sake of validation, we can ignore custom device classes # For the sake of validation, we can ignore custom device classes
@@ -765,7 +777,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return display_precision return display_precision
default_unit_of_measurement = ( default_unit_of_measurement = (
self.suggested_unit_of_measurement or self.native_unit_of_measurement self.suggested_unit_of_measurement
or self.__native_unit_of_measurement_compat
) )
if default_unit_of_measurement is None: if default_unit_of_measurement is None:
return display_precision return display_precision
@@ -843,7 +856,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
(sensor_options := self.registry_entry.options.get(primary_key)) (sensor_options := self.registry_entry.options.get(primary_key))
and secondary_key in sensor_options and secondary_key in sensor_options
and (device_class := self.device_class) in UNIT_CONVERTERS and (device_class := self.device_class) in UNIT_CONVERTERS
and self.native_unit_of_measurement and self.__native_unit_of_measurement_compat
in UNIT_CONVERTERS[device_class].VALID_UNITS in UNIT_CONVERTERS[device_class].VALID_UNITS
and (custom_unit := sensor_options[secondary_key]) and (custom_unit := sensor_options[secondary_key])
in UNIT_CONVERTERS[device_class].VALID_UNITS in UNIT_CONVERTERS[device_class].VALID_UNITS

View File

@@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER, CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@@ -167,7 +168,7 @@ class SensorDeviceClass(StrEnum):
CONDUCTIVITY = "conductivity" CONDUCTIVITY = "conductivity"
"""Conductivity. """Conductivity.
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm` Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
""" """
CURRENT = "current" CURRENT = "current"
@@ -199,7 +200,7 @@ class SensorDeviceClass(StrEnum):
DURATION = "duration" DURATION = "duration"
"""Fixed duration. """Fixed duration.
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs` Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
""" """
ENERGY = "energy" ENERGY = "energy"
@@ -279,25 +280,25 @@ class SensorDeviceClass(StrEnum):
NITROGEN_DIOXIDE = "nitrogen_dioxide" NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2. """Amount of NO2.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
NITROGEN_MONOXIDE = "nitrogen_monoxide" NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO. """Amount of NO.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
NITROUS_OXIDE = "nitrous_oxide" NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O. """Amount of N2O.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
OZONE = "ozone" OZONE = "ozone"
"""Amount of O3. """Amount of O3.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
PH = "ph" PH = "ph"
@@ -309,19 +310,19 @@ class SensorDeviceClass(StrEnum):
PM1 = "pm1" PM1 = "pm1"
"""Particulate matter <= 1 μm. """Particulate matter <= 1 μm.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
PM10 = "pm10" PM10 = "pm10"
"""Particulate matter <= 10 μm. """Particulate matter <= 10 μm.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
PM25 = "pm25" PM25 = "pm25"
"""Particulate matter <= 2.5 μm. """Particulate matter <= 2.5 μm.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
POWER_FACTOR = "power_factor" POWER_FACTOR = "power_factor"
@@ -399,7 +400,7 @@ class SensorDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide" SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2. """Amount of SO2.
Unit of measurement: `µg/m³` Unit of measurement: `μg/m³`
""" """
TEMPERATURE = "temperature" TEMPERATURE = "temperature"
@@ -411,7 +412,7 @@ class SensorDeviceClass(StrEnum):
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""Amount of VOC. """Amount of VOC.
Unit of measurement: `µg/m³`, `mg/m³` Unit of measurement: `μg/m³`, `mg/m³`
""" """
VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
@@ -423,7 +424,7 @@ class SensorDeviceClass(StrEnum):
VOLTAGE = "voltage" VOLTAGE = "voltage"
"""Voltage. """Voltage.
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV` Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
""" """
VOLUME = "volume" VOLUME = "volume"
@@ -470,7 +471,7 @@ class SensorDeviceClass(StrEnum):
Weight is used instead of mass to fit with every day language. Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units Unit of measurement: `MASS_*` units
- SI / metric: `µg`, `mg`, `g`, `kg` - SI / metric: `μg`, `mg`, `g`, `kg`
- USCS / imperial: `oz`, `lb` - USCS / imperial: `oz`, `lb`
""" """
@@ -788,3 +789,16 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] = { STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] = {
SensorStateClass.MEASUREMENT_ANGLE: {DEGREE}, SensorStateClass.MEASUREMENT_ANGLE: {DEGREE},
} }
# We translate units that were using using the legacy coding of μ \u00b5
# to units using recommended coding of μ \u03bc
AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
}

View File

@@ -45,6 +45,7 @@ from homeassistant.util.enum import try_parse_enum
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( from .const import (
AMBIGUOUS_UNITS,
ATTR_LAST_RESET, ATTR_LAST_RESET,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
DOMAIN, DOMAIN,
@@ -79,7 +80,7 @@ EQUIVALENT_UNITS = {
"ft3": UnitOfVolume.CUBIC_FEET, "ft3": UnitOfVolume.CUBIC_FEET,
"m3": UnitOfVolume.CUBIC_METERS, "m3": UnitOfVolume.CUBIC_METERS,
"ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, "ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE,
} } | AMBIGUOUS_UNITS
# Keep track of entities for which a warning about decreasing value has been logged # Keep track of entities for which a warning about decreasing value has been logged

View File

@@ -197,7 +197,7 @@ SENSOR_TYPES = (
attribute=TMRW_ATTR_PRECIPITATION_TYPE, attribute=TMRW_ATTR_PRECIPITATION_TYPE,
value_map=PrecipitationType, value_map=PrecipitationType,
), ),
# Data comes in as ppb, convert to µg/m^3 # Data comes in as ppb, convert to μg/m^3
# Molecular weight of Ozone is 48 # Molecular weight of Ozone is 48
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key="ozone", key="ozone",
@@ -221,7 +221,7 @@ SENSOR_TYPES = (
device_class=SensorDeviceClass.PM10, device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# Data comes in as ppb, convert to µg/m^3 # Data comes in as ppb, convert to μg/m^3
# Molecular weight of Nitrogen Dioxide is 46.01 # Molecular weight of Nitrogen Dioxide is 46.01
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key="nitrogen_dioxide", key="nitrogen_dioxide",
@@ -240,7 +240,7 @@ SENSOR_TYPES = (
device_class=SensorDeviceClass.CO, device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
# Data comes in as ppb, convert to µg/m^3 # Data comes in as ppb, convert to μg/m^3
# Molecular weight of Sulphur Dioxide is 64.07 # Molecular weight of Sulphur Dioxide is 64.07
TomorrowioSensorEntityDescription( TomorrowioSensorEntityDescription(
key="sulphur_dioxide", key="sulphur_dioxide",

View File

@@ -541,7 +541,9 @@ UNITS = (
), ),
UnitOfMeasurement( UnitOfMeasurement(
unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
aliases={"ug/m3", "µg/m3", "ug/m³"}, # The μ-char has 2 unicode variants \u00b5 and \u03bc
# The \u03bc variant (Greek Mu char) is recommended
aliases={"ug/m3", "\u03bcg/m3", "\u00b5g/m3", "ug/m³"},
device_classes={ device_classes={
SensorDeviceClass.NITROGEN_DIOXIDE, SensorDeviceClass.NITROGEN_DIOXIDE,
SensorDeviceClass.NITROGEN_MONOXIDE, SensorDeviceClass.NITROGEN_MONOXIDE,

View File

@@ -671,7 +671,7 @@ class UnitOfElectricCurrent(StrEnum):
class UnitOfElectricPotential(StrEnum): class UnitOfElectricPotential(StrEnum):
"""Electric potential units.""" """Electric potential units."""
MICROVOLT = "µV" MICROVOLT = "μV"
MILLIVOLT = "mV" MILLIVOLT = "mV"
VOLT = "V" VOLT = "V"
KILOVOLT = "kV" KILOVOLT = "kV"
@@ -821,7 +821,7 @@ class UnitOfMass(StrEnum):
GRAMS = "g" GRAMS = "g"
KILOGRAMS = "kg" KILOGRAMS = "kg"
MILLIGRAMS = "mg" MILLIGRAMS = "mg"
MICROGRAMS = "µg" MICROGRAMS = "μg"
OUNCES = "oz" OUNCES = "oz"
POUNDS = "lb" POUNDS = "lb"
STONES = "st" STONES = "st"
@@ -839,13 +839,13 @@ class UnitOfConductivity(
"""Conductivity units.""" """Conductivity units."""
SIEMENS_PER_CM = "S/cm" SIEMENS_PER_CM = "S/cm"
MICROSIEMENS_PER_CM = "µS/cm" MICROSIEMENS_PER_CM = "μS/cm"
MILLISIEMENS_PER_CM = "mS/cm" MILLISIEMENS_PER_CM = "mS/cm"
# Deprecated aliases # Deprecated aliases
SIEMENS = "S/cm" SIEMENS = "S/cm"
"""Deprecated: Please use UnitOfConductivity.SIEMENS_PER_CM""" """Deprecated: Please use UnitOfConductivity.SIEMENS_PER_CM"""
MICROSIEMENS = "µS/cm" MICROSIEMENS = "μS/cm"
"""Deprecated: Please use UnitOfConductivity.MICROSIEMENS_PER_CM""" """Deprecated: Please use UnitOfConductivity.MICROSIEMENS_PER_CM"""
MILLISIEMENS = "mS/cm" MILLISIEMENS = "mS/cm"
"""Deprecated: Please use UnitOfConductivity.MILLISIEMENS_PER_CM""" """Deprecated: Please use UnitOfConductivity.MILLISIEMENS_PER_CM"""
@@ -917,8 +917,8 @@ class UnitOfPrecipitationDepth(StrEnum):
# Concentration units # Concentration units
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³" CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "μg/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³"
CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³"
CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" CONCENTRATION_PARTS_PER_MILLION: Final = "ppm"

View File

@@ -0,0 +1,76 @@
"""Plugin for checking preferred coding of μ is used."""
from __future__ import annotations
from typing import Any
from astroid import nodes
from pylint.checkers import BaseChecker
from pylint.lint import PyLinter
class HassEnforceGreekMicroCharChecker(BaseChecker):
"""Checker for micro char."""
name = "hass-enforce-greek-micro-char"
priority = -1
msgs = {
"W7452": (
"Constants with a micro unit prefix must encode the "
"small Greek Letter Mu as U+03BC (\u03bc), not as U+00B5 (\u00b5)",
"hass-enforce-greek-micro-char",
"According to [The Unicode Consortium]"
"(https://en.wikipedia.org/wiki/Micro-#Symbol_encoding_in_character_sets),"
" the Greek letter character is preferred. "
"To search a specific encoded μ char in Microsoft Visual Studio Code, "
'make sure the "Match case" option is enabled. Note that this only works '
"when searching globally, and not while searching a single document.",
),
}
options = ()
def visit_annassign(self, node: nodes.AnnAssign) -> None:
"""Check for micro char const or StrEnum with type annotations."""
self._do_micro_check(node.target, node)
def visit_assign(self, node: nodes.Assign) -> None:
"""Check for micro char const without type annotations."""
for target in node.targets:
self._do_micro_check(target, node)
def _do_micro_check(
self, target: nodes.NodeNG, node: nodes.Assign | nodes.AnnAssign
) -> None:
"""Check const assignment is not containing ANSI micro char."""
def _check_const(node_const: nodes.Const | Any) -> bool:
if (
isinstance(node_const, nodes.Const)
and isinstance(node_const.value, str)
and "\u00b5" in node_const.value
):
self.add_message(self.name, node=node)
return True
return False
# Check constant assignments
if (
isinstance(target, nodes.AssignName)
and isinstance(node.value, nodes.Const)
and _check_const(node.value)
):
return
# Check dict with EntityDescription calls
if isinstance(target, nodes.AssignName) and isinstance(node.value, nodes.Dict):
for _, subnode in node.value.items:
if not isinstance(subnode, nodes.Call):
continue
for keyword in subnode.keywords:
if _check_const(keyword.value):
return
def register(linter: PyLinter) -> None:
"""Register the checker."""
linter.register_checker(HassEnforceGreekMicroCharChecker(linter))

View File

@@ -121,6 +121,7 @@ load-plugins = [
"hass_async_load_fixtures", "hass_async_load_fixtures",
"hass_decorator", "hass_decorator",
"hass_enforce_class_module", "hass_enforce_class_module",
"hass_enforce_greek_micro_char",
"hass_enforce_sorted_platforms", "hass_enforce_sorted_platforms",
"hass_enforce_super_call", "hass_enforce_super_call",
"hass_enforce_type_hints", "hass_enforce_type_hints",

View File

@@ -622,7 +622,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '84fce612f5b8-pm01', 'unique_id': '84fce612f5b8-pm01',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[indoor][sensor.airgradient_pm1-state] # name: test_all_entities[indoor][sensor.airgradient_pm1-state]
@@ -631,7 +631,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Airgradient PM1', 'friendly_name': 'Airgradient PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airgradient_pm1', 'entity_id': 'sensor.airgradient_pm1',
@@ -675,7 +675,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '84fce612f5b8-pm10', 'unique_id': '84fce612f5b8-pm10',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[indoor][sensor.airgradient_pm10-state] # name: test_all_entities[indoor][sensor.airgradient_pm10-state]
@@ -684,7 +684,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Airgradient PM10', 'friendly_name': 'Airgradient PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airgradient_pm10', 'entity_id': 'sensor.airgradient_pm10',
@@ -728,7 +728,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '84fce612f5b8-pm02', 'unique_id': '84fce612f5b8-pm02',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[indoor][sensor.airgradient_pm2_5-state] # name: test_all_entities[indoor][sensor.airgradient_pm2_5-state]
@@ -737,7 +737,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Airgradient PM2.5', 'friendly_name': 'Airgradient PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airgradient_pm2_5', 'entity_id': 'sensor.airgradient_pm2_5',
@@ -833,7 +833,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'raw_pm02', 'translation_key': 'raw_pm02',
'unique_id': '84fce612f5b8-pm02_raw', 'unique_id': '84fce612f5b8-pm02_raw',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[indoor][sensor.airgradient_raw_pm2_5-state] # name: test_all_entities[indoor][sensor.airgradient_raw_pm2_5-state]
@@ -842,7 +842,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Airgradient Raw PM2.5', 'friendly_name': 'Airgradient Raw PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airgradient_raw_pm2_5', 'entity_id': 'sensor.airgradient_raw_pm2_5',

View File

@@ -36,7 +36,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'co', 'translation_key': 'co',
'unique_id': '123-456-co', 'unique_id': '123-456-co',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_carbon_monoxide-state] # name: test_sensor[sensor.home_carbon_monoxide-state]
@@ -47,7 +47,7 @@
'limit': 4000, 'limit': 4000,
'percent': 4, 'percent': 4,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_carbon_monoxide', 'entity_id': 'sensor.home_carbon_monoxide',
@@ -207,7 +207,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-456-no2', 'unique_id': '123-456-no2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_nitrogen_dioxide-state] # name: test_sensor[sensor.home_nitrogen_dioxide-state]
@@ -219,7 +219,7 @@
'limit': 25, 'limit': 25,
'percent': 64, 'percent': 64,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_dioxide', 'entity_id': 'sensor.home_nitrogen_dioxide',
@@ -266,7 +266,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-456-o3', 'unique_id': '123-456-o3',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_ozone-state] # name: test_sensor[sensor.home_ozone-state]
@@ -278,7 +278,7 @@
'limit': 100, 'limit': 100,
'percent': 42, 'percent': 42,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_ozone', 'entity_id': 'sensor.home_ozone',
@@ -325,7 +325,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-456-pm1', 'unique_id': '123-456-pm1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_pm1-state] # name: test_sensor[sensor.home_pm1-state]
@@ -335,7 +335,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Home PM1', 'friendly_name': 'Home PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_pm1', 'entity_id': 'sensor.home_pm1',
@@ -382,7 +382,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-456-pm10', 'unique_id': '123-456-pm10',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_pm10-state] # name: test_sensor[sensor.home_pm10-state]
@@ -394,7 +394,7 @@
'limit': 45, 'limit': 45,
'percent': 14, 'percent': 14,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_pm10', 'entity_id': 'sensor.home_pm10',
@@ -441,7 +441,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-456-pm25', 'unique_id': '123-456-pm25',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_pm2_5-state] # name: test_sensor[sensor.home_pm2_5-state]
@@ -453,7 +453,7 @@
'limit': 15, 'limit': 15,
'percent': 29, 'percent': 29,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_pm2_5', 'entity_id': 'sensor.home_pm2_5',
@@ -557,7 +557,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-456-so2', 'unique_id': '123-456-so2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_sulphur_dioxide-state] # name: test_sensor[sensor.home_sulphur_dioxide-state]
@@ -569,7 +569,7 @@
'limit': 40, 'limit': 40,
'percent': 35, 'percent': 35,
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_sulphur_dioxide', 'entity_id': 'sensor.home_sulphur_dioxide',

View File

@@ -263,7 +263,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '2960000001_pm1', 'unique_id': '2960000001_pm1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_device_types[view_plus][sensor.living_room_pm1-state] # name: test_all_device_types[view_plus][sensor.living_room_pm1-state]
@@ -272,7 +272,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Living Room PM1', 'friendly_name': 'Living Room PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.living_room_pm1', 'entity_id': 'sensor.living_room_pm1',
@@ -319,7 +319,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '2960000001_pm25', 'unique_id': '2960000001_pm25',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_device_types[view_plus][sensor.living_room_pm2_5-state] # name: test_all_device_types[view_plus][sensor.living_room_pm2_5-state]
@@ -328,7 +328,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Living Room PM2.5', 'friendly_name': 'Living Room PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.living_room_pm2_5', 'entity_id': 'sensor.living_room_pm2_5',

View File

@@ -319,7 +319,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'pm_10', 'translation_key': 'pm_10',
'unique_id': '5366960e8b18-SDS_P1', 'unique_id': '5366960e8b18-SDS_P1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[sensor.5366960e8b18_pm10-state] # name: test_all_entities[sensor.5366960e8b18_pm10-state]
@@ -328,7 +328,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': '5366960e8b18 PM10', 'friendly_name': '5366960e8b18 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.5366960e8b18_pm10', 'entity_id': 'sensor.5366960e8b18_pm10',
@@ -375,7 +375,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'pm_25', 'translation_key': 'pm_25',
'unique_id': '5366960e8b18-SDS_P2', 'unique_id': '5366960e8b18-SDS_P2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[sensor.5366960e8b18_pm2_5-state] # name: test_all_entities[sensor.5366960e8b18_pm2_5-state]
@@ -384,7 +384,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': '5366960e8b18 PM2.5', 'friendly_name': '5366960e8b18 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.5366960e8b18_pm2_5', 'entity_id': 'sensor.5366960e8b18_pm2_5',

View File

@@ -144,7 +144,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'test-serial-number_PM10', 'unique_id': 'test-serial-number_PM10',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[entry_pm2_5] # name: test_sensors[entry_pm2_5]
@@ -181,7 +181,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'test-serial-number_PM25', 'unique_id': 'test-serial-number_PM25',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[entry_temperature] # name: test_sensors[entry_temperature]
@@ -314,7 +314,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Test Sensor PM10', 'friendly_name': 'Test Sensor PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_sensor_pm10', 'entity_id': 'sensor.test_sensor_pm10',
@@ -330,7 +330,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Test Sensor PM2.5', 'friendly_name': 'Test Sensor PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_sensor_pm2_5', 'entity_id': 'sensor.test_sensor_pm2_5',

View File

@@ -253,14 +253,14 @@ _LOGGER = logging.getLogger(__name__)
{ {
"sensor_entity": "sensor.test_device_18b2_pm10", "sensor_entity": "sensor.test_device_18b2_pm10",
"friendly_name": "Test Device 18B2 Pm10", "friendly_name": "Test Device 18B2 Pm10",
"unit_of_measurement": "µg/m³", "unit_of_measurement": "μg/m³",
"state_class": "measurement", "state_class": "measurement",
"expected_state": "7170", "expected_state": "7170",
}, },
{ {
"sensor_entity": "sensor.test_device_18b2_pm25", "sensor_entity": "sensor.test_device_18b2_pm25",
"friendly_name": "Test Device 18B2 Pm25", "friendly_name": "Test Device 18B2 Pm25",
"unit_of_measurement": "µg/m³", "unit_of_measurement": "μg/m³",
"state_class": "measurement", "state_class": "measurement",
"expected_state": "3090", "expected_state": "3090",
}, },
@@ -296,7 +296,7 @@ _LOGGER = logging.getLogger(__name__)
"sensor.test_device_18b2_volatile_organic_compounds" "sensor.test_device_18b2_volatile_organic_compounds"
), ),
"friendly_name": "Test Device 18B2 Volatile Organic Compounds", "friendly_name": "Test Device 18B2 Volatile Organic Compounds",
"unit_of_measurement": "µg/m³", "unit_of_measurement": "μg/m³",
"state_class": "measurement", "state_class": "measurement",
"expected_state": "307", "expected_state": "307",
}, },
@@ -607,14 +607,14 @@ async def test_v1_sensors(
{ {
"sensor_entity": "sensor.test_device_18b2_pm10", "sensor_entity": "sensor.test_device_18b2_pm10",
"friendly_name": "Test Device 18B2 Pm10", "friendly_name": "Test Device 18B2 Pm10",
"unit_of_measurement": "µg/m³", "unit_of_measurement": "μg/m³",
"state_class": "measurement", "state_class": "measurement",
"expected_state": "7170", "expected_state": "7170",
}, },
{ {
"sensor_entity": "sensor.test_device_18b2_pm25", "sensor_entity": "sensor.test_device_18b2_pm25",
"friendly_name": "Test Device 18B2 Pm25", "friendly_name": "Test Device 18B2 Pm25",
"unit_of_measurement": "µg/m³", "unit_of_measurement": "μg/m³",
"state_class": "measurement", "state_class": "measurement",
"expected_state": "3090", "expected_state": "3090",
}, },
@@ -650,7 +650,7 @@ async def test_v1_sensors(
"sensor.test_device_18b2_volatile_organic_compounds" "sensor.test_device_18b2_volatile_organic_compounds"
), ),
"friendly_name": "Test Device 18B2 Volatile Organic Compounds", "friendly_name": "Test Device 18B2 Volatile Organic Compounds",
"unit_of_measurement": "µg/m³", "unit_of_measurement": "μg/m³",
"state_class": "measurement", "state_class": "measurement",
"expected_state": "307", "expected_state": "307",
}, },

View File

@@ -829,7 +829,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042a-particulate_matter_pm2_5', 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042a-particulate_matter_pm2_5',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload14-expected14][sensor.starkvind_airpurifier_pm25-state] # name: test_sensors[config_entry_options0-sensor_payload14-expected14][sensor.starkvind_airpurifier_pm25-state]
@@ -838,7 +838,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'STARKVIND AirPurifier PM25', 'friendly_name': 'STARKVIND AirPurifier PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.starkvind_airpurifier_pm25', 'entity_id': 'sensor.starkvind_airpurifier_pm25',
@@ -1377,7 +1377,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ch2o-state] # name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_ch2o-state]
@@ -1386,7 +1386,7 @@
'device_class': 'volatile_organic_compounds', 'device_class': 'volatile_organic_compounds',
'friendly_name': 'AirQuality 1 CH2O', 'friendly_name': 'AirQuality 1 CH2O',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airquality_1_ch2o', 'entity_id': 'sensor.airquality_1_ch2o',
@@ -1483,7 +1483,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_pm25-state] # name: test_sensors[config_entry_options0-sensor_payload2-expected2][sensor.airquality_1_pm25-state]
@@ -1492,7 +1492,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'AirQuality 1 PM25', 'friendly_name': 'AirQuality 1 PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airquality_1_pm25', 'entity_id': 'sensor.airquality_1_pm25',
@@ -1699,7 +1699,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ch2o-state] # name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_ch2o-state]
@@ -1708,7 +1708,7 @@
'device_class': 'volatile_organic_compounds', 'device_class': 'volatile_organic_compounds',
'friendly_name': 'AirQuality 1 CH2O', 'friendly_name': 'AirQuality 1 CH2O',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airquality_1_ch2o', 'entity_id': 'sensor.airquality_1_ch2o',
@@ -1805,7 +1805,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_pm25-state] # name: test_sensors[config_entry_options0-sensor_payload3-expected3][sensor.airquality_1_pm25-state]
@@ -1814,7 +1814,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'AirQuality 1 PM25', 'friendly_name': 'AirQuality 1 PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airquality_1_pm25', 'entity_id': 'sensor.airquality_1_pm25',
@@ -1910,7 +1910,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ch2o-state] # name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_ch2o-state]
@@ -1919,7 +1919,7 @@
'device_class': 'volatile_organic_compounds', 'device_class': 'volatile_organic_compounds',
'friendly_name': 'AirQuality 1 CH2O', 'friendly_name': 'AirQuality 1 CH2O',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airquality_1_ch2o', 'entity_id': 'sensor.airquality_1_ch2o',
@@ -2016,7 +2016,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_pm25-state] # name: test_sensors[config_entry_options0-sensor_payload4-expected4][sensor.airquality_1_pm25-state]
@@ -2025,7 +2025,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'AirQuality 1 PM25', 'friendly_name': 'AirQuality 1 PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.airquality_1_pm25', 'entity_id': 'sensor.airquality_1_pm25',

View File

@@ -68,8 +68,14 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None:
@pytest.mark.parametrize("platform", ["sensor"]) @pytest.mark.parametrize("platform", ["sensor"])
async def test_options(hass: HomeAssistant, platform) -> None: @pytest.mark.parametrize(
"""Test reconfiguring.""" ("unit_prefix_entry", "unit_prefix_used"),
[("k", "k"), ("\u00b5", "\u03bc"), ("\u03bc", "\u03bc")],
)
async def test_options(
hass: HomeAssistant, platform, unit_prefix_entry: str, unit_prefix_used: str
) -> None:
"""Test reconfiguring and migrated unit prefix."""
# Setup the config entry # Setup the config entry
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
data={}, data={},
@@ -79,7 +85,7 @@ async def test_options(hass: HomeAssistant, platform) -> None:
"round": 1.0, "round": 1.0,
"source": "sensor.input", "source": "sensor.input",
"time_window": {"seconds": 0.0}, "time_window": {"seconds": 0.0},
"unit_prefix": "k", "unit_prefix": unit_prefix_entry,
"unit_time": "min", "unit_time": "min",
"max_sub_interval": {"seconds": 30}, "max_sub_interval": {"seconds": 30},
}, },
@@ -99,7 +105,7 @@ async def test_options(hass: HomeAssistant, platform) -> None:
schema = result["data_schema"].schema schema = result["data_schema"].schema
assert get_schema_suggested_value(schema, "round") == 1.0 assert get_schema_suggested_value(schema, "round") == 1.0
assert get_schema_suggested_value(schema, "time_window") == {"seconds": 0.0} assert get_schema_suggested_value(schema, "time_window") == {"seconds": 0.0}
assert get_schema_suggested_value(schema, "unit_prefix") == "k" assert get_schema_suggested_value(schema, "unit_prefix") == unit_prefix_used
assert get_schema_suggested_value(schema, "unit_time") == "min" assert get_schema_suggested_value(schema, "unit_time") == "min"
source = schema["source"] source = schema["source"]

View File

@@ -519,7 +519,7 @@ async def test_migration_1_1(hass: HomeAssistant, unit_prefix, expect_prefix) ->
assert config_entry.options.get("unit_prefix") == expect_prefix assert config_entry.options.get("unit_prefix") == expect_prefix
assert config_entry.version == 1 assert config_entry.version == 1
assert config_entry.minor_version == 3 assert config_entry.minor_version == 4
async def test_migration_1_2( async def test_migration_1_2(
@@ -570,7 +570,44 @@ async def test_migration_1_2(
assert derivative_entity_entry.device_id == sensor_entity_entry.device_id assert derivative_entity_entry.device_id == sensor_entity_entry.device_id
assert derivative_config_entry.version == 1 assert derivative_config_entry.version == 1
assert derivative_config_entry.minor_version == 3 assert derivative_config_entry.minor_version == 4
@pytest.mark.parametrize(
("unit_prefix", "expect_prefix"),
[
({"unit_prefix": "\u00b5"}, "\u03bc"),
({"unit_prefix": "\u03bc"}, "\u03bc"),
],
)
async def test_migration_1_4(hass: HomeAssistant, unit_prefix, expect_prefix) -> None:
"""Test migration from v1.4 migrates to Greek Mu char" unit_prefix."""
config_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={
"name": "My derivative",
"round": 1.0,
"source": "sensor.power",
"time_window": {"seconds": 0.0},
**unit_prefix,
"unit_time": "min",
},
title="My derivative",
version=1,
minor_version=1,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.options["unit_time"] == "min"
assert config_entry.options.get("unit_prefix") == expect_prefix
assert config_entry.version == 1
assert config_entry.minor_version == 4
async def test_migration_from_future_version( async def test_migration_from_future_version(

View File

@@ -103,7 +103,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'c6h6', 'translation_key': 'c6h6',
'unique_id': '123-c6h6', 'unique_id': '123-c6h6',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_benzene-state] # name: test_sensor[sensor.home_benzene-state]
@@ -112,7 +112,7 @@
'attribution': 'Data provided by GIOŚ', 'attribution': 'Data provided by GIOŚ',
'friendly_name': 'Home Benzene', 'friendly_name': 'Home Benzene',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_benzene', 'entity_id': 'sensor.home_benzene',
@@ -159,7 +159,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'co', 'translation_key': 'co',
'unique_id': '123-co', 'unique_id': '123-co',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_carbon_monoxide-state] # name: test_sensor[sensor.home_carbon_monoxide-state]
@@ -168,7 +168,7 @@
'attribution': 'Data provided by GIOŚ', 'attribution': 'Data provided by GIOŚ',
'friendly_name': 'Home Carbon monoxide', 'friendly_name': 'Home Carbon monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_carbon_monoxide', 'entity_id': 'sensor.home_carbon_monoxide',
@@ -215,7 +215,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-no2', 'unique_id': '123-no2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_nitrogen_dioxide-state] # name: test_sensor[sensor.home_nitrogen_dioxide-state]
@@ -225,7 +225,7 @@
'device_class': 'nitrogen_dioxide', 'device_class': 'nitrogen_dioxide',
'friendly_name': 'Home Nitrogen dioxide', 'friendly_name': 'Home Nitrogen dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_dioxide', 'entity_id': 'sensor.home_nitrogen_dioxide',
@@ -339,7 +339,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-no', 'unique_id': '123-no',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_nitrogen_monoxide-state] # name: test_sensor[sensor.home_nitrogen_monoxide-state]
@@ -349,7 +349,7 @@
'device_class': 'nitrogen_monoxide', 'device_class': 'nitrogen_monoxide',
'friendly_name': 'Home Nitrogen monoxide', 'friendly_name': 'Home Nitrogen monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_monoxide', 'entity_id': 'sensor.home_nitrogen_monoxide',
@@ -396,7 +396,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'nox', 'translation_key': 'nox',
'unique_id': '123-nox', 'unique_id': '123-nox',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_nitrogen_oxides-state] # name: test_sensor[sensor.home_nitrogen_oxides-state]
@@ -405,7 +405,7 @@
'attribution': 'Data provided by GIOŚ', 'attribution': 'Data provided by GIOŚ',
'friendly_name': 'Home Nitrogen oxides', 'friendly_name': 'Home Nitrogen oxides',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_oxides', 'entity_id': 'sensor.home_nitrogen_oxides',
@@ -452,7 +452,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-o3', 'unique_id': '123-o3',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_ozone-state] # name: test_sensor[sensor.home_ozone-state]
@@ -462,7 +462,7 @@
'device_class': 'ozone', 'device_class': 'ozone',
'friendly_name': 'Home Ozone', 'friendly_name': 'Home Ozone',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_ozone', 'entity_id': 'sensor.home_ozone',
@@ -576,7 +576,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-pm10', 'unique_id': '123-pm10',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_pm10-state] # name: test_sensor[sensor.home_pm10-state]
@@ -586,7 +586,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Home PM10', 'friendly_name': 'Home PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_pm10', 'entity_id': 'sensor.home_pm10',
@@ -700,7 +700,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-pm25', 'unique_id': '123-pm25',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_pm2_5-state] # name: test_sensor[sensor.home_pm2_5-state]
@@ -710,7 +710,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Home PM2.5', 'friendly_name': 'Home PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_pm2_5', 'entity_id': 'sensor.home_pm2_5',
@@ -824,7 +824,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '123-so2', 'unique_id': '123-so2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.home_sulphur_dioxide-state] # name: test_sensor[sensor.home_sulphur_dioxide-state]
@@ -834,7 +834,7 @@
'device_class': 'sulphur_dioxide', 'device_class': 'sulphur_dioxide',
'friendly_name': 'Home Sulphur dioxide', 'friendly_name': 'Home Sulphur dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.home_sulphur_dioxide', 'entity_id': 'sensor.home_sulphur_dioxide',

View File

@@ -183,7 +183,7 @@ async def test_gvh5106(hass: HomeAssistant) -> None:
pm25_sensor_attributes = pm25_sensor.attributes pm25_sensor_attributes = pm25_sensor.attributes
assert pm25_sensor.state == "0" assert pm25_sensor.state == "0"
assert pm25_sensor_attributes[ATTR_FRIENDLY_NAME] == "H5106 4E05 Pm25" assert pm25_sensor_attributes[ATTR_FRIENDLY_NAME] == "H5106 4E05 Pm25"
assert pm25_sensor_attributes[ATTR_UNIT_OF_MEASUREMENT] == "µg/m³" assert pm25_sensor_attributes[ATTR_UNIT_OF_MEASUREMENT] == "μg/m³"
assert pm25_sensor_attributes[ATTR_STATE_CLASS] == "measurement" assert pm25_sensor_attributes[ATTR_STATE_CLASS] == "measurement"
assert await hass.config_entries.async_unload(entry.entry_id) assert await hass.config_entries.async_unload(entry.entry_id)

View File

@@ -363,14 +363,14 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00:00:00:00:00:00_1_2576_2580', 'unique_id': '00:00:00:00:00:00_1_2576_2580',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'state': dict({ 'state': dict({
'attributes': dict({ 'attributes': dict({
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Airversa AP2 1808 PM2.5 Density', 'friendly_name': 'Airversa AP2 1808 PM2.5 Density',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'entity_id': 'sensor.airversa_ap2_1808_pm2_5_density', 'entity_id': 'sensor.airversa_ap2_1808_pm2_5_density',
'state': '3.0', 'state': '3.0',

View File

@@ -94,7 +94,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': <PinecilNumber.CALIBRATION_OFFSET: 'calibration_offset'>, 'translation_key': <PinecilNumber.CALIBRATION_OFFSET: 'calibration_offset'>,
'unique_id': 'c0:ff:ee:c0:ff:ee_calibration_offset', 'unique_id': 'c0:ff:ee:c0:ff:ee_calibration_offset',
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'µV'>, 'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'μV'>,
}) })
# --- # ---
# name: test_state[number.pinecil_calibration_offset-state] # name: test_state[number.pinecil_calibration_offset-state]
@@ -105,7 +105,7 @@
'min': 100, 'min': 100,
'mode': <NumberMode.BOX: 'box'>, 'mode': <NumberMode.BOX: 'box'>,
'step': 1, 'step': 1,
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'µV'>, 'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'μV'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'number.pinecil_calibration_offset', 'entity_id': 'number.pinecil_calibration_offset',

View File

@@ -566,7 +566,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': <PinecilSensor.TIP_VOLTAGE: 'tip_voltage'>, 'translation_key': <PinecilSensor.TIP_VOLTAGE: 'tip_voltage'>,
'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage', 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage',
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'µV'>, 'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'μV'>,
}) })
# --- # ---
# name: test_sensors[sensor.pinecil_raw_tip_voltage-state] # name: test_sensors[sensor.pinecil_raw_tip_voltage-state]
@@ -575,7 +575,7 @@
'device_class': 'voltage', 'device_class': 'voltage',
'friendly_name': 'Pinecil Raw tip voltage', 'friendly_name': 'Pinecil Raw tip voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'µV'>, 'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'μV'>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.pinecil_raw_tip_voltage', 'entity_id': 'sensor.pinecil_raw_tip_voltage',

View File

@@ -135,7 +135,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1', 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[sensor.test_air_conditioner_pm1-state] # name: test_all_entities[sensor.test_air_conditioner_pm1-state]
@@ -144,7 +144,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Test air conditioner PM1', 'friendly_name': 'Test air conditioner PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_air_conditioner_pm1', 'entity_id': 'sensor.test_air_conditioner_pm1',
@@ -188,7 +188,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10', 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[sensor.test_air_conditioner_pm10-state] # name: test_all_entities[sensor.test_air_conditioner_pm10-state]
@@ -197,7 +197,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Test air conditioner PM10', 'friendly_name': 'Test air conditioner PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_air_conditioner_pm10', 'entity_id': 'sensor.test_air_conditioner_pm10',
@@ -241,7 +241,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2', 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[sensor.test_air_conditioner_pm2_5-state] # name: test_all_entities[sensor.test_air_conditioner_pm2_5-state]
@@ -250,7 +250,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Test air conditioner PM2.5', 'friendly_name': 'Test air conditioner PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.test_air_conditioner_pm2_5', 'entity_id': 'sensor.test_air_conditioner_pm2_5',

View File

@@ -468,7 +468,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM1Sensor-1068-0', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM1Sensor-1068-0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[air_purifier][sensor.air_purifier_pm1-state] # name: test_sensors[air_purifier][sensor.air_purifier_pm1-state]
@@ -477,7 +477,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Air Purifier PM1', 'friendly_name': 'Air Purifier PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.air_purifier_pm1', 'entity_id': 'sensor.air_purifier_pm1',
@@ -521,7 +521,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM10Sensor-1069-0', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM10Sensor-1069-0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[air_purifier][sensor.air_purifier_pm10-state] # name: test_sensors[air_purifier][sensor.air_purifier_pm10-state]
@@ -530,7 +530,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Air Purifier PM10', 'friendly_name': 'Air Purifier PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.air_purifier_pm10', 'entity_id': 'sensor.air_purifier_pm10',
@@ -574,7 +574,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM25Sensor-1066-0', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM25Sensor-1066-0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[air_purifier][sensor.air_purifier_pm2_5-state] # name: test_sensors[air_purifier][sensor.air_purifier_pm2_5-state]
@@ -583,7 +583,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Air Purifier PM2.5', 'friendly_name': 'Air Purifier PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.air_purifier_pm2_5', 'entity_id': 'sensor.air_purifier_pm2_5',
@@ -1017,7 +1017,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM1Sensor-1068-0', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM1Sensor-1068-0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm1-state] # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm1-state]
@@ -1026,7 +1026,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'lightfi-aq1-air-quality-sensor PM1', 'friendly_name': 'lightfi-aq1-air-quality-sensor PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm1', 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm1',
@@ -1070,7 +1070,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM10Sensor-1069-0', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM10Sensor-1069-0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm10-state] # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm10-state]
@@ -1079,7 +1079,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'lightfi-aq1-air-quality-sensor PM10', 'friendly_name': 'lightfi-aq1-air-quality-sensor PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm10', 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm10',
@@ -1123,7 +1123,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM25Sensor-1066-0', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM25Sensor-1066-0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm2_5-state] # name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_pm2_5-state]
@@ -1132,7 +1132,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'lightfi-aq1-air-quality-sensor PM2.5', 'friendly_name': 'lightfi-aq1-air-quality-sensor PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm2_5', 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm2_5',

View File

@@ -26,6 +26,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_MODE, ATTR_MODE,
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
UnitOfElectricPotential,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
@@ -253,6 +254,62 @@ async def test_native_value_validation(
mqtt_mock.async_publish.reset_mock() mqtt_mock.async_publish.reset_mock()
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
number.DOMAIN: {
"name": "test",
"command_topic": "test-topic-cmd",
"state_topic": "test-topic",
"unit_of_measurement": "\u00b5V",
}
}
}
],
)
async def test_equivalent_unit_of_measurement(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device_class with equivalent unit of measurement."""
assert await mqtt_mock_entry()
async_fire_mqtt_message(hass, "test-topic", "100")
await hass.async_block_till_done()
state = hass.states.get("number.test")
assert state is not None
assert state.state == "100"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
is UnitOfElectricPotential.MICROVOLT
)
caplog.clear()
discovery_payload = {
"name": "bla",
"command_topic": "test-topic2-cmd",
"state_topic": "test-topic2",
"unit_of_measurement": "\u00b5V",
}
# Now discover an invalid sensor
async_fire_mqtt_message(
hass, "homeassistant/number/bla/config", json.dumps(discovery_payload)
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "test-topic2", "21")
await hass.async_block_till_done()
state = hass.states.get("number.bla")
assert state is not None
assert state.state == "21"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
is UnitOfElectricPotential.MICROVOLT
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"hass_config", "hass_config",
[ [

View File

@@ -15,9 +15,11 @@ import pytest
from homeassistant.components import mqtt, sensor from homeassistant.components import mqtt, sensor
from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
UnitOfElectricPotential,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.core import Event, HomeAssistant, State, callback
@@ -906,6 +908,116 @@ async def test_invalid_unit_of_measurement(
assert state is None assert state is None
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "voltage",
"unit_of_measurement": "\u00b5V", # microVolt
}
}
}
],
)
async def test_device_class_with_equivalent_unit_of_measurement_received(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device_class with equivalent unit of measurement."""
assert await mqtt_mock_entry()
async_fire_mqtt_message(hass, "test-topic", "100")
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state is not None
assert state.state == "100"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
is UnitOfElectricPotential.MICROVOLT
)
caplog.clear()
discovery_payload = {
"name": "bla",
"state_topic": "test-topic2",
"device_class": "voltage",
"unit_of_measurement": "\u00b5V",
}
# Now discover a sensor with an altarantive mu char
async_fire_mqtt_message(
hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload)
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "test-topic2", "21")
await hass.async_block_till_done()
state = hass.states.get("sensor.bla")
assert state is not None
assert state.state == "21"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
is UnitOfElectricPotential.MICROVOLT
)
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"unit_of_measurement": "\u00b5V",
}
}
}
],
)
async def test_equivalent_unit_of_measurement_received_without_device_class(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device_class with equivalent unit of measurement."""
assert await mqtt_mock_entry()
async_fire_mqtt_message(hass, "test-topic", "100")
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state is not None
assert state.state == "100"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
is UnitOfElectricPotential.MICROVOLT
)
caplog.clear()
discovery_payload = {
"name": "bla",
"state_topic": "test-topic2",
"unit_of_measurement": "\u00b5V",
}
# Now discover an invalid sensor
async_fire_mqtt_message(
hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload)
)
await hass.async_block_till_done()
async_fire_mqtt_message(hass, "test-topic2", "21")
await hass.async_block_till_done()
state = hass.states.get("sensor.bla")
assert state is not None
assert state.state == "21"
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
is UnitOfElectricPotential.MICROVOLT
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"hass_config", "hass_config",
[ [

View File

@@ -981,7 +981,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'pmsx003_pm1', 'translation_key': 'pmsx003_pm1',
'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-state] # name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-state]
@@ -990,7 +990,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Nettigo Air Monitor PMSx003 PM1', 'friendly_name': 'Nettigo Air Monitor PMSx003 PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1', 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1',
@@ -1037,7 +1037,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'pmsx003_pm10', 'translation_key': 'pmsx003_pm10',
'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-state] # name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-state]
@@ -1046,7 +1046,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Nettigo Air Monitor PMSx003 PM10', 'friendly_name': 'Nettigo Air Monitor PMSx003 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10', 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10',
@@ -1093,7 +1093,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'pmsx003_pm25', 'translation_key': 'pmsx003_pm25',
'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-state] # name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-state]
@@ -1102,7 +1102,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Nettigo Air Monitor PMSx003 PM2.5', 'friendly_name': 'Nettigo Air Monitor PMSx003 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5', 'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5',
@@ -1261,7 +1261,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'sds011_pm10', 'translation_key': 'sds011_pm10',
'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-state] # name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-state]
@@ -1270,7 +1270,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Nettigo Air Monitor SDS011 PM10', 'friendly_name': 'Nettigo Air Monitor SDS011 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10', 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10',
@@ -1317,7 +1317,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'sds011_pm25', 'translation_key': 'sds011_pm25',
'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-state] # name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-state]
@@ -1326,7 +1326,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Nettigo Air Monitor SDS011 PM2.5', 'friendly_name': 'Nettigo Air Monitor SDS011 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5', 'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5',
@@ -1653,7 +1653,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'sps30_pm1', 'translation_key': 'sps30_pm1',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-state] # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-state]
@@ -1662,7 +1662,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': 'Nettigo Air Monitor SPS30 PM1', 'friendly_name': 'Nettigo Air Monitor SPS30 PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1', 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1',
@@ -1709,7 +1709,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'sps30_pm10', 'translation_key': 'sps30_pm10',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-state] # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-state]
@@ -1718,7 +1718,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Nettigo Air Monitor SPS30 PM10', 'friendly_name': 'Nettigo Air Monitor SPS30 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10', 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10',
@@ -1765,7 +1765,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'sps30_pm25', 'translation_key': 'sps30_pm25',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-state] # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-state]
@@ -1774,7 +1774,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Nettigo Air Monitor SPS30 PM2.5', 'friendly_name': 'Nettigo Air Monitor SPS30 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5', 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5',
@@ -1821,7 +1821,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': 'sps30_pm4', 'translation_key': 'sps30_pm4',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-state] # name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-state]
@@ -1829,7 +1829,7 @@
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'Nettigo Air Monitor SPS30 PM4', 'friendly_name': 'Nettigo Air Monitor SPS30 PM4',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4', 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4',

View File

@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from homeassistant.components.number import ( from homeassistant.components.number import (
AMBIGUOUS_UNITS,
ATTR_MAX, ATTR_MAX,
ATTR_MIN, ATTR_MIN,
ATTR_MODE, ATTR_MODE,
@@ -48,6 +49,7 @@ from . import common
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
MockEntity,
MockModule, MockModule,
MockPlatform, MockPlatform,
async_mock_restore_state_shutdown_restart, async_mock_restore_state_shutdown_restart,
@@ -61,6 +63,25 @@ from tests.common import (
TEST_DOMAIN = "test" TEST_DOMAIN = "test"
class MockNumber(MockEntity, NumberEntity):
"""Mock NumberEntity class to test unit of measurement."""
@property
def device_class(self):
"""Return the class of this sensor."""
return self._handle("device_class")
@property
def native_unit_of_measurement(self):
"""Return the native unit_of_measurement of this sensor."""
return self._handle("native_unit_of_measurement")
@property
def native_value(self):
"""Return the native value of this sensor."""
return self._handle("native_value")
class MockDefaultNumberEntity(NumberEntity): class MockDefaultNumberEntity(NumberEntity):
"""Mock NumberEntity device to use in tests. """Mock NumberEntity device to use in tests.
@@ -900,6 +921,33 @@ async def test_translated_unit_with_native_unit_raises(
assert entity0.entity_id is None assert entity0.entity_id is None
@pytest.mark.parametrize(
("ambiguous_unit", "normalized_unit"),
[
(ambiguous_unit, normalized_unit)
for ambiguous_unit, normalized_unit in AMBIGUOUS_UNITS.items()
],
)
async def test_ambiguous_unit_of_measurement_compat(
hass: HomeAssistant, ambiguous_unit: str, normalized_unit: str
) -> None:
"""Test ambiguous native_unit_of_measurement values are corrected."""
entity0 = MockNumber(
name="Test",
native_value="0.0",
native_unit_of_measurement=ambiguous_unit,
)
setup_test_component_platform(hass, DOMAIN, [entity0])
assert await async_setup_component(hass, "number", {"number": {"platform": "test"}})
await hass.async_block_till_done()
# Check compatible unit is applied
state = hass.states.get(entity0.entity_id)
assert state.state == "0.0"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit
def test_device_classes_aligned() -> None: def test_device_classes_aligned() -> None:
"""Make sure all sensor device classes are also available in NumberDeviceClass.""" """Make sure all sensor device classes are also available in NumberDeviceClass."""

View File

@@ -86,7 +86,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-co', 'unique_id': '12.34-56.78-co',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state]
@@ -96,7 +96,7 @@
'device_class': 'carbon_monoxide', 'device_class': 'carbon_monoxide',
'friendly_name': 'openweathermap Carbon monoxide', 'friendly_name': 'openweathermap Carbon monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_carbon_monoxide', 'entity_id': 'sensor.openweathermap_carbon_monoxide',
@@ -140,7 +140,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-no2', 'unique_id': '12.34-56.78-no2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-state]
@@ -150,7 +150,7 @@
'device_class': 'nitrogen_dioxide', 'device_class': 'nitrogen_dioxide',
'friendly_name': 'openweathermap Nitrogen dioxide', 'friendly_name': 'openweathermap Nitrogen dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_nitrogen_dioxide', 'entity_id': 'sensor.openweathermap_nitrogen_dioxide',
@@ -194,7 +194,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-no', 'unique_id': '12.34-56.78-no',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-state]
@@ -204,7 +204,7 @@
'device_class': 'nitrogen_monoxide', 'device_class': 'nitrogen_monoxide',
'friendly_name': 'openweathermap Nitrogen monoxide', 'friendly_name': 'openweathermap Nitrogen monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_nitrogen_monoxide', 'entity_id': 'sensor.openweathermap_nitrogen_monoxide',
@@ -248,7 +248,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-o3', 'unique_id': '12.34-56.78-o3',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-state]
@@ -258,7 +258,7 @@
'device_class': 'ozone', 'device_class': 'ozone',
'friendly_name': 'openweathermap Ozone', 'friendly_name': 'openweathermap Ozone',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_ozone', 'entity_id': 'sensor.openweathermap_ozone',
@@ -302,7 +302,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-pm10', 'unique_id': '12.34-56.78-pm10',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-state]
@@ -312,7 +312,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'openweathermap PM10', 'friendly_name': 'openweathermap PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_pm10', 'entity_id': 'sensor.openweathermap_pm10',
@@ -356,7 +356,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-pm2_5', 'unique_id': '12.34-56.78-pm2_5',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-state]
@@ -366,7 +366,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'openweathermap PM2.5', 'friendly_name': 'openweathermap PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_pm2_5', 'entity_id': 'sensor.openweathermap_pm2_5',
@@ -410,7 +410,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '12.34-56.78-so2', 'unique_id': '12.34-56.78-so2',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-state] # name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-state]
@@ -420,7 +420,7 @@
'device_class': 'sulphur_dioxide', 'device_class': 'sulphur_dioxide',
'friendly_name': 'openweathermap Sulphur dioxide', 'friendly_name': 'openweathermap Sulphur dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.openweathermap_sulphur_dioxide', 'entity_id': 'sensor.openweathermap_sulphur_dioxide',

View File

@@ -776,7 +776,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '01:03:05:07:12:34-pm25', 'unique_id': '01:03:05:07:12:34-pm25',
'unit_of_measurement': <Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 'µg/m³'>, 'unit_of_measurement': g/m³',
}) })
# --- # ---
# name: test_sensors[v6][sensor.ruuvitag_884f_pm2_5-state] # name: test_sensors[v6][sensor.ruuvitag_884f_pm2_5-state]
@@ -785,7 +785,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'RuuviTag 884F PM2.5', 'friendly_name': 'RuuviTag 884F PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 'µg/m³'>, 'unit_of_measurement': g/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.ruuvitag_884f_pm2_5', 'entity_id': 'sensor.ruuvitag_884f_pm2_5',

View File

@@ -11,8 +11,12 @@ from unittest.mock import patch
import pytest import pytest
from homeassistant.components import sensor from homeassistant.components import sensor
from homeassistant.components.number import NumberDeviceClass from homeassistant.components.number import (
AMBIGUOUS_UNITS as NUMBER_AMBIGUOUS_UNITS,
NumberDeviceClass,
)
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
AMBIGUOUS_UNITS as SENSOR_AMBIGUOUS_UNITS,
DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_STATE_CLASSES,
DEVICE_CLASS_UNITS, DEVICE_CLASS_UNITS,
DOMAIN, DOMAIN,
@@ -159,12 +163,47 @@ async def test_temperature_conversion_wrong_device_class(
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done() await hass.async_block_till_done()
# Check temperature is not converted # Check compatible unit is applied
state = hass.states.get(entity0.entity_id) state = hass.states.get(entity0.entity_id)
assert state.state == "0.0" assert state.state == "0.0"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT
@pytest.mark.parametrize(
("ambiguous_unit", "normalized_unit"),
[
(ambiguous_unit, normalized_unit)
for ambiguous_unit, normalized_unit in sensor.AMBIGUOUS_UNITS.items()
],
)
async def test_ambiguous_unit_of_measurement_compat(
hass: HomeAssistant, ambiguous_unit: str, normalized_unit: str
) -> None:
"""Test ambiguous native_unit_of_measurement values are corrected."""
entity0 = MockSensor(
name="Test",
native_value="0.0",
native_unit_of_measurement=ambiguous_unit,
)
setup_test_component_platform(hass, sensor.DOMAIN, [entity0])
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
# Check temperature is not converted
state = hass.states.get(entity0.entity_id)
assert state.state == "0.0"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit
def test_ambiguous_units_of_measurement_aligned() -> None:
"""Make sure all ambiguous UOM for sensor are also available for number."""
for ambiguous_unit, unit in SENSOR_AMBIGUOUS_UNITS.items():
assert ambiguous_unit in NUMBER_AMBIGUOUS_UNITS
assert NUMBER_AMBIGUOUS_UNITS[ambiguous_unit] == unit
@pytest.mark.parametrize("state_class", ["measurement", "total_increasing"]) @pytest.mark.parametrize("state_class", ["measurement", "total_increasing"])
async def test_deprecated_last_reset( async def test_deprecated_last_reset(
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -3755,6 +3755,44 @@ async def test_compile_hourly_statistics_convert_units_1(
30, 30,
), ),
(None, "m3", "", None, "volume", 13.050847, 13.333333, -10, 30), (None, "m3", "", None, "volume", 13.050847, 13.333333, -10, 30),
(None, "\u00b5V", "\u03bcV", None, "voltage", 13.050847, 13.333333, -10, 30),
(None, "\u00b5Sv/h", "\u03bcSv/h", None, None, 13.050847, 13.333333, -10, 30),
(
None,
"\u00b5S/cm",
"\u03bcS/cm",
None,
"conductivity",
13.050847,
13.333333,
-10,
30,
),
(None, "\u00b5g/ft³", "\u03bcg/ft³", None, None, 13.050847, 13.333333, -10, 30),
(
None,
"\u00b5g/m³",
"\u03bcg/m³",
None,
"concentration",
13.050847,
13.333333,
-10,
30,
),
(
None,
"\u00b5mol/s⋅m²",
"\u03bcmol/s⋅m²",
None,
None,
13.050847,
13.333333,
-10,
30,
),
(None, "\u00b5g", "\u03bcg", None, "mass", 13.050847, 13.333333, -10, 30),
(None, "\u00b5s", "\u03bcs", None, "duration", 13.050847, 13.333333, -10, 30),
], ],
) )
async def test_compile_hourly_statistics_equivalent_units_1( async def test_compile_hourly_statistics_equivalent_units_1(
@@ -3884,6 +3922,17 @@ async def test_compile_hourly_statistics_equivalent_units_1(
(None, "ft3", "ft³", None, 13.333333, -10, 30), (None, "ft3", "ft³", None, 13.333333, -10, 30),
(None, "ft³/m", "ft³/min", None, 13.333333, -10, 30), (None, "ft³/m", "ft³/min", None, 13.333333, -10, 30),
(None, "m3", "", None, 13.333333, -10, 30), (None, "m3", "", None, 13.333333, -10, 30),
(None, "\u00b5V", "\u03bcV", None, 13.333333, -10, 30),
(SensorDeviceClass.VOLTAGE, "\u00b5V", "\u03bcV", None, 13.333333, -10, 30),
(None, "\u00b5Sv/h", "\u03bcSv/h", None, 13.333333, -10, 30),
(None, "\u00b5S/cm", "\u03bcS/cm", None, 13.333333, -10, 30),
(None, "\u00b5g/ft³", "\u03bcg/ft³", None, 13.333333, -10, 30),
(None, "\u00b5g/m³", "\u03bcg/m³", None, 13.333333, -10, 30),
(None, "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²", None, 13.333333, -10, 30),
(None, "\u00b5g", "\u03bcg", None, 13.333333, -10, 30),
(SensorDeviceClass.WEIGHT, "\u00b5g", "\u03bcg", None, 13.333333, -10, 30),
(None, "\u00b5s", "\u03bcs", None, 13.333333, -10, 30),
(SensorDeviceClass.DURATION, "\u00b5s", "\u03bcs", None, 13.333333, -10, 30),
], ],
) )
async def test_compile_hourly_statistics_equivalent_units_2( async def test_compile_hourly_statistics_equivalent_units_2(
@@ -5705,6 +5754,14 @@ async def test_validate_statistics_unit_change_no_conversion(
(NONE_SENSOR_ATTRIBUTES, "m3", ""), (NONE_SENSOR_ATTRIBUTES, "m3", ""),
(NONE_SENSOR_ATTRIBUTES, "rpm", "RPM"), (NONE_SENSOR_ATTRIBUTES, "rpm", "RPM"),
(NONE_SENSOR_ATTRIBUTES, "RPM", "rpm"), (NONE_SENSOR_ATTRIBUTES, "RPM", "rpm"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5V", "\u03bcV"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5Sv/h", "\u03bcSv/h"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5S/cm", "\u03bcS/cm"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5g/ft³", "\u03bcg/ft³"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5g/m³", "\u03bcg/m³"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5g", "\u03bcg"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5s", "\u03bcs"),
], ],
) )
async def test_validate_statistics_unit_change_equivalent_units( async def test_validate_statistics_unit_change_equivalent_units(
@@ -5768,6 +5825,15 @@ async def test_validate_statistics_unit_change_equivalent_units(
("attributes", "unit1", "unit2", "supported_unit"), ("attributes", "unit1", "unit2", "supported_unit"),
[ [
(NONE_SENSOR_ATTRIBUTES, "", "m3", "CCF, L, fl. oz., ft³, gal, mL, m³"), (NONE_SENSOR_ATTRIBUTES, "", "m3", "CCF, L, fl. oz., ft³, gal, mL, m³"),
(NONE_SENSOR_ATTRIBUTES, "\u03bcV", "\u00b5V", "MV, V, kV, mV, \u03bcV"),
(NONE_SENSOR_ATTRIBUTES, "\u03bcS/cm", "\u00b5S/cm", "S/cm, mS/cm, \u03bcS/cm"),
(
NONE_SENSOR_ATTRIBUTES,
"\u03bcg",
"\u00b5g",
"g, kg, lb, mg, oz, st, \u03bcg",
),
(NONE_SENSOR_ATTRIBUTES, "\u03bcs", "\u00b5s", "d, h, min, ms, s, w, \u03bcs"),
], ],
) )
async def test_validate_statistics_unit_change_equivalent_units_2( async def test_validate_statistics_unit_change_equivalent_units_2(

View File

@@ -250,7 +250,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main_fineDustSensor_fineDustLevel_fineDustLevel', 'unique_id': 'e44d4e5c-45ea-498f-a653-f5d0c3d97bb8_main_fineDustSensor_fineDustLevel_fineDustLevel',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_pm2_5-state] # name: test_all_entities[aq_sensor_3_ikea][sensor.aq_sensor_3_ikea_pm2_5-state]
@@ -259,7 +259,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'aq-sensor-3-ikea PM2.5', 'friendly_name': 'aq-sensor-3-ikea PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.aq_sensor_3_ikea_pm2_5', 'entity_id': 'sensor.aq_sensor_3_ikea_pm2_5',
@@ -1166,7 +1166,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_veryFineDustSensor_veryFineDustLevel_veryFineDustLevel', 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_veryFineDustSensor_veryFineDustLevel_veryFineDustLevel',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm1-state] # name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm1-state]
@@ -1175,7 +1175,7 @@
'device_class': 'pm1', 'device_class': 'pm1',
'friendly_name': '에어모니터 플러스 PM1', 'friendly_name': '에어모니터 플러스 PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.eeomoniteo_peulreoseu_pm1', 'entity_id': 'sensor.eeomoniteo_peulreoseu_pm1',
@@ -1219,7 +1219,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_dustLevel_dustLevel', 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_dustLevel_dustLevel',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm10-state] # name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm10-state]
@@ -1228,7 +1228,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': '에어모니터 플러스 PM10', 'friendly_name': '에어모니터 플러스 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.eeomoniteo_peulreoseu_pm10', 'entity_id': 'sensor.eeomoniteo_peulreoseu_pm10',
@@ -1272,7 +1272,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_fineDustLevel_fineDustLevel', 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_fineDustLevel_fineDustLevel',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm2_5-state] # name: test_all_entities[da_ac_airsensor_01001][sensor.eeomoniteo_peulreoseu_pm2_5-state]
@@ -1281,7 +1281,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': '에어모니터 플러스 PM2.5', 'friendly_name': '에어모니터 플러스 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.eeomoniteo_peulreoseu_pm2_5', 'entity_id': 'sensor.eeomoniteo_peulreoseu_pm2_5',
@@ -3035,7 +3035,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_dustLevel_dustLevel', 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_dustLevel_dustLevel',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[da_ac_rac_100001][sensor.corridor_a_c_pm10-state] # name: test_all_entities[da_ac_rac_100001][sensor.corridor_a_c_pm10-state]
@@ -3044,7 +3044,7 @@
'device_class': 'pm10', 'device_class': 'pm10',
'friendly_name': 'Corridor A/C PM10', 'friendly_name': 'Corridor A/C PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.corridor_a_c_pm10', 'entity_id': 'sensor.corridor_a_c_pm10',
@@ -3088,7 +3088,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_fineDustLevel_fineDustLevel', 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_fineDustLevel_fineDustLevel',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}) })
# --- # ---
# name: test_all_entities[da_ac_rac_100001][sensor.corridor_a_c_pm2_5-state] # name: test_all_entities[da_ac_rac_100001][sensor.corridor_a_c_pm2_5-state]
@@ -3097,7 +3097,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Corridor A/C PM2.5', 'friendly_name': 'Corridor A/C PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.corridor_a_c_pm2_5', 'entity_id': 'sensor.corridor_a_c_pm2_5',

View File

@@ -355,7 +355,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '400s-purifier-pm25', 'unique_id': '400s-purifier-pm25',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
]) ])
# --- # ---
@@ -393,7 +393,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Air Purifier 400s PM2.5', 'friendly_name': 'Air Purifier 400s PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.air_purifier_400s_pm2_5', 'entity_id': 'sensor.air_purifier_400s_pm2_5',
@@ -539,7 +539,7 @@
'supported_features': 0, 'supported_features': 0,
'translation_key': None, 'translation_key': None,
'unique_id': '600s-purifier-pm25', 'unique_id': '600s-purifier-pm25',
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
]) ])
# --- # ---
@@ -577,7 +577,7 @@
'device_class': 'pm25', 'device_class': 'pm25',
'friendly_name': 'Air Purifier 600s PM2.5', 'friendly_name': 'Air Purifier 600s PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>, 'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³', 'unit_of_measurement': 'μg/m³',
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'sensor.air_purifier_600s_pm2_5', 'entity_id': 'sensor.air_purifier_600s_pm2_5',

View File

@@ -262,7 +262,7 @@ async def test_xiaomi_hhccjcy01(hass: HomeAssistant) -> None:
cond_sensor_attribtes = cond_sensor.attributes cond_sensor_attribtes = cond_sensor.attributes
assert cond_sensor.state == "599" assert cond_sensor.state == "599"
assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity"
assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm"
assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement"
moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture")
@@ -351,7 +351,7 @@ async def test_xiaomi_hhccjcy01_not_connectable(hass: HomeAssistant) -> None:
cond_sensor_attribtes = cond_sensor.attributes cond_sensor_attribtes = cond_sensor.attributes
assert cond_sensor.state == "599" assert cond_sensor.state == "599"
assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity"
assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm"
assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement"
moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture")
@@ -438,7 +438,7 @@ async def test_xiaomi_hhccjcy01_only_some_sources_connectable(
cond_sensor_attribtes = cond_sensor.attributes cond_sensor_attribtes = cond_sensor.attributes
assert cond_sensor.state == "599" assert cond_sensor.state == "599"
assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity" assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 3E7A Conductivity"
assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm"
assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement"
moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture") moist_sensor = hass.states.get("sensor.plant_sensor_3e7a_moisture")
@@ -653,7 +653,7 @@ async def test_hhccjcy10_uuid(hass: HomeAssistant) -> None:
cond_sensor_attr = cond_sensor.attributes cond_sensor_attr = cond_sensor.attributes
assert cond_sensor.state == "91" assert cond_sensor.state == "91"
assert cond_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Conductivity" assert cond_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 5BFC Conductivity"
assert cond_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "μS/cm"
assert cond_sensor_attr[ATTR_STATE_CLASS] == "measurement" assert cond_sensor_attr[ATTR_STATE_CLASS] == "measurement"
moist_sensor = hass.states.get("sensor.plant_sensor_5bfc_moisture") moist_sensor = hass.states.get("sensor.plant_sensor_5bfc_moisture")

View File

@@ -138,3 +138,24 @@ def decorator_checker_fixture(hass_decorator, linter) -> BaseChecker:
type_hint_checker = hass_decorator.HassDecoratorChecker(linter) type_hint_checker = hass_decorator.HassDecoratorChecker(linter)
type_hint_checker.module = "homeassistant.components.pylint_test" type_hint_checker.module = "homeassistant.components.pylint_test"
return type_hint_checker return type_hint_checker
@pytest.fixture(name="hass_enforce_greek_micro_char", scope="package")
def hass_enforce_greek_micro_checker_fixture() -> ModuleType:
"""Fixture to the content for the hass_enforce_greek_micro_char check."""
return _load_plugin_from_file(
"hass_enforce_greek_micro_char",
"pylint/plugins/hass_enforce_greek_micro_char.py",
)
@pytest.fixture(name="enforce_greek_micro_char_checker")
def enforce_greek_micro_char_checker_fixture(
hass_enforce_greek_micro_char, linter
) -> BaseChecker:
"""Fixture to provide a hass_enforce_greek_micro_char checker."""
enforce_greek_micro_char_checker = (
hass_enforce_greek_micro_char.HassEnforceGreekMicroCharChecker(linter)
)
enforce_greek_micro_char_checker.module = "homeassistant.components.pylint_test"
return enforce_greek_micro_char_checker

View File

@@ -0,0 +1,164 @@
"""Tests for pylint hass_enforce_greek_micro_char plugin."""
from __future__ import annotations
import astroid
from pylint.checkers import BaseChecker
from pylint.testutils.unittest_linter import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
import pytest
from . import assert_no_messages
@pytest.mark.parametrize(
"code",
[
pytest.param(
# Test using the correct μ-sign \u03bc with annotation
"""
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "μg/m³"
""",
id="good_const_with_annotation",
),
pytest.param(
# Test using the correct μ-sign \u03bc with annotation using unicode encoding
"""
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "\u03bcg/m³"
""",
id="good_unicode_const_with_annotation",
),
pytest.param(
# Test using the correct μ-sign \u03bc without annotation
"""
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "μg/m³"
""",
id="good_const_without_annotation",
),
pytest.param(
# Test using the correct μ-sign \u03bc in a StrEnum class
"""
class UnitOfElectricPotential(StrEnum):
\"\"\"Electric potential units.\"\"\"
MICROVOLT = "μV"
MILLIVOLT = "mV"
VOLT = "V"
KILOVOLT = "kV"
MEGAVOLT = "MV"
""",
id="good_str_enum",
),
pytest.param(
# Test using the correct μ-sign \u03bc in a sensor description dict
"""
SENSOR_DESCRIPTION = {
"radiation_rate": AranetSensorEntityDescription(
key="radiation_rate",
translation_key="radiation_rate",
name="Radiation Dose Rate",
native_unit_of_measurement="μSv/h",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
scale=0.001,
),
}
OTHER_DICT = {
"value_with_bad_mu_should_pass": "µ"
}
""",
id="good_sensor_description",
),
],
)
def test_enforce_greek_micro_char(
linter: UnittestLinter,
enforce_greek_micro_char_checker: BaseChecker,
code: str,
) -> None:
"""Good test cases."""
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
walker = ASTWalker(linter)
walker.add_checker(enforce_greek_micro_char_checker)
with assert_no_messages(linter):
walker.walk(root_node)
@pytest.mark.parametrize(
"code",
[
pytest.param(
# Test we can detect the legacy coding of μ \u00b5
# instead of recommended coding of μ \u03bc" with annotation
"""
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
""",
id="bad_const_with_annotation",
),
pytest.param(
# Test we can detect the unicode variant of the legacy coding of μ \u00b5
# instead of recommended coding of μ \u03bc" with annotation
"""
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "\u00b5g/m³"
""",
id="bad_unicode_const_with_annotation",
),
pytest.param(
# Test we can detect the legacy coding of μ \u00b5
# instead of recommended coding of μ \u03bc" without annotation
"""
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
""",
id="bad_const_without_annotation",
),
pytest.param(
# Test we can detect the legacy coding of μ \u00b5
# instead of recommended coding of μ \u03bc" in a StrEnum class
"""
class UnitOfElectricPotential(StrEnum):
\"\"\"Electric potential units.\"\"\"
MICROVOLT = "µV"
MILLIVOLT = "mV"
VOLT = "V"
KILOVOLT = "kV"
MEGAVOLT = "MV"
""",
id="bad_str_enum",
),
pytest.param(
# Test we can detect the legacy coding of μ \u00b5
# instead of recommended coding of μ \u03bc" in a sensor description dict
"""
SENSOR_DESCRIPTION = {
"radiation_rate": AranetSensorEntityDescription(
key="radiation_rate",
translation_key="radiation_rate",
name="Radiation Dose Rate",
native_unit_of_measurement="µSv/h",
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=2,
scale=0.001,
),
}
""",
id="bad_sensor_description",
),
],
)
def test_enforce_greek_micro_char_assign_bad(
linter: UnittestLinter,
enforce_greek_micro_char_checker: BaseChecker,
code: str,
) -> None:
"""Bad assignment test cases."""
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
walker = ASTWalker(linter)
walker.add_checker(enforce_greek_micro_char_checker)
walker.walk(root_node)
messages = linter.release_messages()
assert len(messages) == 1
message = next(iter(messages))
assert message.msg_id == "hass-enforce-greek-micro-char"

View File

@@ -118,7 +118,7 @@ def test_deprecated_unit_of_conductivity_alias() -> None:
"""Test UnitOfConductivity deprecation.""" """Test UnitOfConductivity deprecation."""
# Test the deprecated members are aliases # Test the deprecated members are aliases
assert set(const.UnitOfConductivity) == {"S/cm", "µS/cm", "mS/cm"} assert set(const.UnitOfConductivity) == {"S/cm", "μS/cm", "mS/cm"}
def test_deprecated_unit_of_conductivity_members( def test_deprecated_unit_of_conductivity_members(