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": {
"name": "Display PM standard",
"state": {
"ugm3": "µg/m³",
"ugm3": "μg/m³",
"us_aqi": "US AQI"
}
},

View File

@@ -72,7 +72,7 @@ SENSOR_DESCRIPTIONS = {
key=str(BTHomeExtendedSensorDeviceClass.CHANNEL),
state_class=SensorStateClass.MEASUREMENT,
),
# Conductivity (µS/cm)
# Conductivity (μS/cm)
(
BTHomeSensorDeviceClass.CONDUCTIVITY,
Units.CONDUCTIVITY,
@@ -215,7 +215,7 @@ SENSOR_DESCRIPTIONS = {
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
# PM10 (µg/m3)
# PM10 (μg/m3)
(
BTHomeSensorDeviceClass.PM10,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -225,7 +225,7 @@ SENSOR_DESCRIPTIONS = {
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
# PM2.5 (µg/m3)
# PM2.5 (μg/m3)
(
BTHomeSensorDeviceClass.PM25,
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -318,7 +318,7 @@ SENSOR_DESCRIPTIONS = {
key=str(BTHomeSensorDeviceClass.UV_INDEX),
state_class=SensorStateClass.MEASUREMENT,
),
# Volatile organic Compounds (VOC) (µg/m3)
# Volatile organic Compounds (VOC) (μg/m3)
(
BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
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
)
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(
"Migration to configuration version %s.%s successful",
config_entry.version,

View File

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

View File

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

View File

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

View File

@@ -291,7 +291,7 @@ class NitrogenDioxideSensor(AirQualitySensor):
class VolatileOrganicCompoundsSensor(AirQualitySensor):
"""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:

View File

@@ -494,7 +494,7 @@ def temperature_to_states(temperature: float, unit: str) -> float:
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)
return 1
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:
"""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)
return 1
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:
"""Map nitrogen dioxide µg/m3 to HomeKit AirQuality level."""
"""Map nitrogen dioxide μg/m3 to HomeKit AirQuality level."""
if density <= 30:
return 1
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:
"""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).
Referenced from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf

View File

@@ -16,6 +16,7 @@ from homeassistant.components.number import (
NumberMode,
RestoreNumber,
)
from homeassistant.components.sensor import AMBIGUOUS_UNITS
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DEVICE_CLASS,
@@ -70,6 +71,12 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
def validate_config(config: ConfigType) -> ConfigType:
"""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]:
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.sensor import (
AMBIGUOUS_UNITS,
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
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}'"
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None:
return config
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
if (

View File

@@ -31,6 +31,7 @@ from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401
AMBIGUOUS_UNITS,
ATTR_MAX,
ATTR_MIN,
ATTR_STEP,
@@ -368,6 +369,15 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self.entity_description.native_unit_of_measurement
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
@final
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:
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
# of the time we can avoid the device_class check
if (
@@ -444,7 +454,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if device_class not in UNIT_CONVERTERS:
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
if native_unit_of_measurement != unit_of_measurement:
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:
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
if native_unit_of_measurement != unit_of_measurement:
if TYPE_CHECKING:
@@ -496,7 +506,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
(number_options := self.registry_entry.options.get(DOMAIN))
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
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
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
):

View File

@@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@@ -136,7 +137,7 @@ class NumberDeviceClass(StrEnum):
CONDUCTIVITY = "conductivity"
"""Conductivity.
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm`
Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
"""
CURRENT = "current"
@@ -168,7 +169,7 @@ class NumberDeviceClass(StrEnum):
DURATION = "duration"
"""Fixed duration.
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs`
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
"""
ENERGY = "energy"
@@ -246,25 +247,25 @@ class NumberDeviceClass(StrEnum):
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
PH = "ph"
@@ -276,19 +277,19 @@ class NumberDeviceClass(StrEnum):
PM1 = "pm1"
"""Particulate matter <= 1 μm.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
PM10 = "pm10"
"""Particulate matter <= 10 μm.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
PM25 = "pm25"
"""Particulate matter <= 2.5 μm.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
POWER_FACTOR = "power_factor"
@@ -365,7 +366,7 @@ class NumberDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -377,7 +378,7 @@ class NumberDeviceClass(StrEnum):
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""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"
@@ -389,7 +390,7 @@ class NumberDeviceClass(StrEnum):
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV`
Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
"""
VOLUME = "volume"
@@ -436,7 +437,7 @@ class NumberDeviceClass(StrEnum):
Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units
- SI / metric: `µg`, `mg`, `g`, `kg`
- SI / metric: `μg`, `mg`, `g`, `kg`
- USCS / imperial: `oz`, `lb`
"""
@@ -556,3 +557,16 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
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
def _sanitize_metric_name(metric: str) -> str:
metric.replace("\u03bc", "\u00b5")
return "".join(
[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",
}
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()
return units.get(unit, default)

View File

@@ -261,7 +261,7 @@ def correct_db_schema_precision(
from ..migration import _modify_columns # noqa: PLC0415
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
engine = instance.engine
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 .const import ( # noqa: F401
AMBIGUOUS_UNITS,
ATTR_LAST_RESET,
ATTR_OPTIONS,
ATTR_STATE_CLASS,
@@ -314,7 +315,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return _numeric_state_expected(
try_parse_enum(SensorDeviceClass, self.device_class),
self.state_class,
self.native_unit_of_measurement,
self.__native_unit_of_measurement_compat,
self.suggested_display_precision,
)
@@ -366,7 +367,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Make sure we can convert the units
if (
(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
):
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:
# Fallback to unit suggested by the unit conversion rules from device class
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 (
@@ -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,
# fall back to the unit suggested by the unit converter's unit class.
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:
@@ -468,6 +470,16 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self.entity_description.native_unit_of_measurement
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
def suggested_unit_of_measurement(self) -> str | None:
"""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:
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
if not self.registry_entry and (
@@ -543,7 +555,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@override
def state(self) -> Any:
"""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
value = self.native_value
# 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
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:
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))
and secondary_key in sensor_options
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
and (custom_unit := sensor_options[secondary_key])
in UNIT_CONVERTERS[device_class].VALID_UNITS

View File

@@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@@ -167,7 +168,7 @@ class SensorDeviceClass(StrEnum):
CONDUCTIVITY = "conductivity"
"""Conductivity.
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm`
Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
"""
CURRENT = "current"
@@ -199,7 +200,7 @@ class SensorDeviceClass(StrEnum):
DURATION = "duration"
"""Fixed duration.
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs`
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
"""
ENERGY = "energy"
@@ -279,25 +280,25 @@ class SensorDeviceClass(StrEnum):
NITROGEN_DIOXIDE = "nitrogen_dioxide"
"""Amount of NO2.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
NITROGEN_MONOXIDE = "nitrogen_monoxide"
"""Amount of NO.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
NITROUS_OXIDE = "nitrous_oxide"
"""Amount of N2O.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
OZONE = "ozone"
"""Amount of O3.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
PH = "ph"
@@ -309,19 +310,19 @@ class SensorDeviceClass(StrEnum):
PM1 = "pm1"
"""Particulate matter <= 1 μm.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
PM10 = "pm10"
"""Particulate matter <= 10 μm.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
PM25 = "pm25"
"""Particulate matter <= 2.5 μm.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
POWER_FACTOR = "power_factor"
@@ -399,7 +400,7 @@ class SensorDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `µg/m³`
Unit of measurement: `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -411,7 +412,7 @@ class SensorDeviceClass(StrEnum):
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
"""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"
@@ -423,7 +424,7 @@ class SensorDeviceClass(StrEnum):
VOLTAGE = "voltage"
"""Voltage.
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV`
Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
"""
VOLUME = "volume"
@@ -470,7 +471,7 @@ class SensorDeviceClass(StrEnum):
Weight is used instead of mass to fit with every day language.
Unit of measurement: `MASS_*` units
- SI / metric: `µg`, `mg`, `g`, `kg`
- SI / metric: `μg`, `mg`, `g`, `kg`
- 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]] = {
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 .const import (
AMBIGUOUS_UNITS,
ATTR_LAST_RESET,
ATTR_STATE_CLASS,
DOMAIN,
@@ -79,7 +80,7 @@ EQUIVALENT_UNITS = {
"ft3": UnitOfVolume.CUBIC_FEET,
"m3": UnitOfVolume.CUBIC_METERS,
"ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE,
}
} | AMBIGUOUS_UNITS
# 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,
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
TomorrowioSensorEntityDescription(
key="ozone",
@@ -221,7 +221,7 @@ SENSOR_TYPES = (
device_class=SensorDeviceClass.PM10,
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
TomorrowioSensorEntityDescription(
key="nitrogen_dioxide",
@@ -240,7 +240,7 @@ SENSOR_TYPES = (
device_class=SensorDeviceClass.CO,
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
TomorrowioSensorEntityDescription(
key="sulphur_dioxide",

View File

@@ -541,7 +541,9 @@ UNITS = (
),
UnitOfMeasurement(
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={
SensorDeviceClass.NITROGEN_DIOXIDE,
SensorDeviceClass.NITROGEN_MONOXIDE,

View File

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

View File

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

View File

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

View File

@@ -263,7 +263,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -272,7 +272,7 @@
'device_class': 'pm1',
'friendly_name': 'Living Room PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.living_room_pm1',
@@ -319,7 +319,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -328,7 +328,7 @@
'device_class': 'pm25',
'friendly_name': 'Living Room PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.living_room_pm2_5',

View File

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

View File

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

View File

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

View File

@@ -829,7 +829,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -838,7 +838,7 @@
'device_class': 'pm25',
'friendly_name': 'STARKVIND AirPurifier PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.starkvind_airpurifier_pm25',
@@ -1377,7 +1377,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1386,7 +1386,7 @@
'device_class': 'volatile_organic_compounds',
'friendly_name': 'AirQuality 1 CH2O',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.airquality_1_ch2o',
@@ -1483,7 +1483,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1492,7 +1492,7 @@
'device_class': 'pm25',
'friendly_name': 'AirQuality 1 PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.airquality_1_pm25',
@@ -1699,7 +1699,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1708,7 +1708,7 @@
'device_class': 'volatile_organic_compounds',
'friendly_name': 'AirQuality 1 CH2O',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.airquality_1_ch2o',
@@ -1805,7 +1805,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1814,7 +1814,7 @@
'device_class': 'pm25',
'friendly_name': 'AirQuality 1 PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.airquality_1_pm25',
@@ -1910,7 +1910,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1919,7 +1919,7 @@
'device_class': 'volatile_organic_compounds',
'friendly_name': 'AirQuality 1 CH2O',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.airquality_1_ch2o',
@@ -2016,7 +2016,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -2025,7 +2025,7 @@
'device_class': 'pm25',
'friendly_name': 'AirQuality 1 PM25',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'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"])
async def test_options(hass: HomeAssistant, platform) -> None:
"""Test reconfiguring."""
@pytest.mark.parametrize(
("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
config_entry = MockConfigEntry(
data={},
@@ -79,7 +85,7 @@ async def test_options(hass: HomeAssistant, platform) -> None:
"round": 1.0,
"source": "sensor.input",
"time_window": {"seconds": 0.0},
"unit_prefix": "k",
"unit_prefix": unit_prefix_entry,
"unit_time": "min",
"max_sub_interval": {"seconds": 30},
},
@@ -99,7 +105,7 @@ async def test_options(hass: HomeAssistant, platform) -> None:
schema = result["data_schema"].schema
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, "unit_prefix") == "k"
assert get_schema_suggested_value(schema, "unit_prefix") == unit_prefix_used
assert get_schema_suggested_value(schema, "unit_time") == "min"
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.version == 1
assert config_entry.minor_version == 3
assert config_entry.minor_version == 4
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_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(

View File

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

View File

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

View File

@@ -94,7 +94,7 @@
'supported_features': 0,
'translation_key': <PinecilNumber.CALIBRATION_OFFSET: '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]
@@ -105,7 +105,7 @@
'min': 100,
'mode': <NumberMode.BOX: 'box'>,
'step': 1,
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'µV'>,
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'μV'>,
}),
'context': <ANY>,
'entity_id': 'number.pinecil_calibration_offset',

View File

@@ -566,7 +566,7 @@
'supported_features': 0,
'translation_key': <PinecilSensor.TIP_VOLTAGE: '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]
@@ -575,7 +575,7 @@
'device_class': 'voltage',
'friendly_name': 'Pinecil Raw tip voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'µV'>,
'unit_of_measurement': <UnitOfElectricPotential.MICROVOLT: 'μV'>,
}),
'context': <ANY>,
'entity_id': 'sensor.pinecil_raw_tip_voltage',

View File

@@ -135,7 +135,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -144,7 +144,7 @@
'device_class': 'pm1',
'friendly_name': 'Test air conditioner PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.test_air_conditioner_pm1',
@@ -188,7 +188,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -197,7 +197,7 @@
'device_class': 'pm10',
'friendly_name': 'Test air conditioner PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.test_air_conditioner_pm10',
@@ -241,7 +241,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -250,7 +250,7 @@
'device_class': 'pm25',
'friendly_name': 'Test air conditioner PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.test_air_conditioner_pm2_5',

View File

@@ -468,7 +468,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -477,7 +477,7 @@
'device_class': 'pm1',
'friendly_name': 'Air Purifier PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.air_purifier_pm1',
@@ -521,7 +521,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -530,7 +530,7 @@
'device_class': 'pm10',
'friendly_name': 'Air Purifier PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.air_purifier_pm10',
@@ -574,7 +574,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -583,7 +583,7 @@
'device_class': 'pm25',
'friendly_name': 'Air Purifier PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.air_purifier_pm2_5',
@@ -1017,7 +1017,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1026,7 +1026,7 @@
'device_class': 'pm1',
'friendly_name': 'lightfi-aq1-air-quality-sensor PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm1',
@@ -1070,7 +1070,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1079,7 +1079,7 @@
'device_class': 'pm10',
'friendly_name': 'lightfi-aq1-air-quality-sensor PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm10',
@@ -1123,7 +1123,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1132,7 +1132,7 @@
'device_class': 'pm25',
'friendly_name': 'lightfi-aq1-air-quality-sensor PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm2_5',

View File

@@ -26,6 +26,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
ATTR_UNIT_OF_MEASUREMENT,
UnitOfElectricPotential,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, State
@@ -253,6 +254,62 @@ async def test_native_value_validation(
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(
"hass_config",
[

View File

@@ -15,9 +15,11 @@ import pytest
from homeassistant.components import mqtt, sensor
from homeassistant.components.mqtt.sensor import MQTT_SENSOR_ATTRIBUTES_BLOCKED
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfElectricPotential,
UnitOfTemperature,
)
from homeassistant.core import Event, HomeAssistant, State, callback
@@ -906,6 +908,116 @@ async def test_invalid_unit_of_measurement(
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(
"hass_config",
[

View File

@@ -981,7 +981,7 @@
'supported_features': 0,
'translation_key': 'pmsx003_pm1',
'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]
@@ -990,7 +990,7 @@
'device_class': 'pm1',
'friendly_name': 'Nettigo Air Monitor PMSx003 PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1',
@@ -1037,7 +1037,7 @@
'supported_features': 0,
'translation_key': 'pmsx003_pm10',
'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]
@@ -1046,7 +1046,7 @@
'device_class': 'pm10',
'friendly_name': 'Nettigo Air Monitor PMSx003 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10',
@@ -1093,7 +1093,7 @@
'supported_features': 0,
'translation_key': 'pmsx003_pm25',
'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]
@@ -1102,7 +1102,7 @@
'device_class': 'pm25',
'friendly_name': 'Nettigo Air Monitor PMSx003 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5',
@@ -1261,7 +1261,7 @@
'supported_features': 0,
'translation_key': 'sds011_pm10',
'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]
@@ -1270,7 +1270,7 @@
'device_class': 'pm10',
'friendly_name': 'Nettigo Air Monitor SDS011 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10',
@@ -1317,7 +1317,7 @@
'supported_features': 0,
'translation_key': 'sds011_pm25',
'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]
@@ -1326,7 +1326,7 @@
'device_class': 'pm25',
'friendly_name': 'Nettigo Air Monitor SDS011 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5',
@@ -1653,7 +1653,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm1',
'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]
@@ -1662,7 +1662,7 @@
'device_class': 'pm1',
'friendly_name': 'Nettigo Air Monitor SPS30 PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1',
@@ -1709,7 +1709,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm10',
'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]
@@ -1718,7 +1718,7 @@
'device_class': 'pm10',
'friendly_name': 'Nettigo Air Monitor SPS30 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10',
@@ -1765,7 +1765,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm25',
'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]
@@ -1774,7 +1774,7 @@
'device_class': 'pm25',
'friendly_name': 'Nettigo Air Monitor SPS30 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5',
@@ -1821,7 +1821,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm4',
'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]
@@ -1829,7 +1829,7 @@
'attributes': ReadOnlyDict({
'friendly_name': 'Nettigo Air Monitor SPS30 PM4',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4',

View File

@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.number import (
AMBIGUOUS_UNITS,
ATTR_MAX,
ATTR_MIN,
ATTR_MODE,
@@ -48,6 +49,7 @@ from . import common
from tests.common import (
MockConfigEntry,
MockEntity,
MockModule,
MockPlatform,
async_mock_restore_state_shutdown_restart,
@@ -61,6 +63,25 @@ from tests.common import (
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):
"""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
@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:
"""Make sure all sensor device classes are also available in NumberDeviceClass."""

View File

@@ -86,7 +86,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -96,7 +96,7 @@
'device_class': 'carbon_monoxide',
'friendly_name': 'openweathermap Carbon monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_carbon_monoxide',
@@ -140,7 +140,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -150,7 +150,7 @@
'device_class': 'nitrogen_dioxide',
'friendly_name': 'openweathermap Nitrogen dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_nitrogen_dioxide',
@@ -194,7 +194,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -204,7 +204,7 @@
'device_class': 'nitrogen_monoxide',
'friendly_name': 'openweathermap Nitrogen monoxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_nitrogen_monoxide',
@@ -248,7 +248,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -258,7 +258,7 @@
'device_class': 'ozone',
'friendly_name': 'openweathermap Ozone',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_ozone',
@@ -302,7 +302,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -312,7 +312,7 @@
'device_class': 'pm10',
'friendly_name': 'openweathermap PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_pm10',
@@ -356,7 +356,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -366,7 +366,7 @@
'device_class': 'pm25',
'friendly_name': 'openweathermap PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_pm2_5',
@@ -410,7 +410,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -420,7 +420,7 @@
'device_class': 'sulphur_dioxide',
'friendly_name': 'openweathermap Sulphur dioxide',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.openweathermap_sulphur_dioxide',

View File

@@ -776,7 +776,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -785,7 +785,7 @@
'device_class': 'pm25',
'friendly_name': 'RuuviTag 884F PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 'µg/m³'>,
'unit_of_measurement': g/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.ruuvitag_884f_pm2_5',

View File

@@ -11,8 +11,12 @@ from unittest.mock import patch
import pytest
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 (
AMBIGUOUS_UNITS as SENSOR_AMBIGUOUS_UNITS,
DEVICE_CLASS_STATE_CLASSES,
DEVICE_CLASS_UNITS,
DOMAIN,
@@ -159,12 +163,47 @@ async def test_temperature_conversion_wrong_device_class(
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
# Check temperature is not converted
# Check compatible unit is applied
state = hass.states.get(entity0.entity_id)
assert state.state == "0.0"
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"])
async def test_deprecated_last_reset(
hass: HomeAssistant,

View File

@@ -3755,6 +3755,44 @@ async def test_compile_hourly_statistics_convert_units_1(
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(
@@ -3884,6 +3922,17 @@ async def test_compile_hourly_statistics_equivalent_units_1(
(None, "ft3", "ft³", None, 13.333333, -10, 30),
(None, "ft³/m", "ft³/min", 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(
@@ -5705,6 +5754,14 @@ async def test_validate_statistics_unit_change_no_conversion(
(NONE_SENSOR_ATTRIBUTES, "m3", ""),
(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(
@@ -5768,6 +5825,15 @@ async def test_validate_statistics_unit_change_equivalent_units(
("attributes", "unit1", "unit2", "supported_unit"),
[
(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(

View File

@@ -250,7 +250,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -259,7 +259,7 @@
'device_class': 'pm25',
'friendly_name': 'aq-sensor-3-ikea PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.aq_sensor_3_ikea_pm2_5',
@@ -1166,7 +1166,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1175,7 +1175,7 @@
'device_class': 'pm1',
'friendly_name': '에어모니터 플러스 PM1',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.eeomoniteo_peulreoseu_pm1',
@@ -1219,7 +1219,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1228,7 +1228,7 @@
'device_class': 'pm10',
'friendly_name': '에어모니터 플러스 PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.eeomoniteo_peulreoseu_pm10',
@@ -1272,7 +1272,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -1281,7 +1281,7 @@
'device_class': 'pm25',
'friendly_name': '에어모니터 플러스 PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.eeomoniteo_peulreoseu_pm2_5',
@@ -3035,7 +3035,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -3044,7 +3044,7 @@
'device_class': 'pm10',
'friendly_name': 'Corridor A/C PM10',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.corridor_a_c_pm10',
@@ -3088,7 +3088,7 @@
'supported_features': 0,
'translation_key': None,
'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]
@@ -3097,7 +3097,7 @@
'device_class': 'pm25',
'friendly_name': 'Corridor A/C PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.corridor_a_c_pm2_5',

View File

@@ -355,7 +355,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '400s-purifier-pm25',
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
])
# ---
@@ -393,7 +393,7 @@
'device_class': 'pm25',
'friendly_name': 'Air Purifier 400s PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'entity_id': 'sensor.air_purifier_400s_pm2_5',
@@ -539,7 +539,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '600s-purifier-pm25',
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
])
# ---
@@ -577,7 +577,7 @@
'device_class': 'pm25',
'friendly_name': 'Air Purifier 600s PM2.5',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'µg/m³',
'unit_of_measurement': 'μg/m³',
}),
'context': <ANY>,
'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
assert cond_sensor.state == "599"
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"
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
assert cond_sensor.state == "599"
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"
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
assert cond_sensor.state == "599"
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"
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
assert cond_sensor.state == "91"
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"
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.module = "homeassistant.components.pylint_test"
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 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(