From 48300f4563b9cb76031b3991cb95c66a3577ad0f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 19 Aug 2025 18:48:50 +0200 Subject: [PATCH] Use greek small letter mu "\u03bc" instead of micro sign "\u00B5" for micro unit prefix (alt 1) (#144853) Co-authored-by: Erik Montnemery --- .../components/airgradient/strings.json | 2 +- homeassistant/components/bthome/sensor.py | 8 +- .../components/derivative/__init__.py | 12 ++ .../components/derivative/config_flow.py | 4 +- homeassistant/components/derivative/sensor.py | 2 +- homeassistant/components/emoncms/sensor.py | 2 +- .../components/homekit/type_sensors.py | 2 +- homeassistant/components/homekit/util.py | 8 +- homeassistant/components/mqtt/number.py | 7 + homeassistant/components/mqtt/sensor.py | 12 +- homeassistant/components/number/__init__.py | 18 +- homeassistant/components/number/const.py | 40 +++-- .../components/prometheus/__init__.py | 4 + .../recorder/auto_repairs/schema.py | 2 +- homeassistant/components/sensor/__init__.py | 29 +++- homeassistant/components/sensor/const.py | 40 +++-- homeassistant/components/sensor/recorder.py | 3 +- homeassistant/components/tomorrowio/sensor.py | 6 +- homeassistant/components/tuya/const.py | 4 +- homeassistant/const.py | 10 +- .../plugins/hass_enforce_greek_micro_char.py | 76 ++++++++ pyproject.toml | 1 + .../airgradient/snapshots/test_sensor.ambr | 16 +- .../airly/snapshots/test_sensor.ambr | 28 +-- .../airthings/snapshots/test_sensor.ambr | 8 +- .../altruist/snapshots/test_sensor.ambr | 8 +- .../arve/snapshots/test_sensor.ambr | 8 +- tests/components/bthome/test_sensor.py | 12 +- .../deconz/snapshots/test_sensor.ambr | 28 +-- .../components/derivative/test_config_flow.py | 14 +- tests/components/derivative/test_init.py | 41 ++++- .../gios/snapshots/test_sensor.ambr | 36 ++-- tests/components/govee_ble/test_sensor.py | 2 +- .../snapshots/test_init.ambr | 4 +- .../iron_os/snapshots/test_number.ambr | 4 +- .../iron_os/snapshots/test_sensor.ambr | 4 +- .../lg_thinq/snapshots/test_sensor.ambr | 12 +- .../matter/snapshots/test_sensor.ambr | 24 +-- tests/components/mqtt/test_number.py | 57 ++++++ tests/components/mqtt/test_sensor.py | 112 ++++++++++++ .../components/nam/snapshots/test_sensor.ambr | 36 ++-- tests/components/number/test_init.py | 48 +++++ .../openweathermap/snapshots/test_sensor.ambr | 28 +-- .../ruuvitag_ble/snapshots/test_sensor.ambr | 4 +- tests/components/sensor/test_init.py | 43 ++++- tests/components/sensor/test_recorder.py | 66 +++++++ .../smartthings/snapshots/test_sensor.ambr | 24 +-- .../vesync/snapshots/test_sensor.ambr | 8 +- tests/components/xiaomi_ble/test_sensor.py | 8 +- tests/pylint/conftest.py | 21 +++ tests/pylint/test_enforce_greek_micro_char.py | 164 ++++++++++++++++++ tests/test_const.py | 2 +- 52 files changed, 936 insertions(+), 226 deletions(-) create mode 100644 pylint/plugins/hass_enforce_greek_micro_char.py create mode 100644 tests/pylint/test_enforce_greek_micro_char.py diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index cef4db57358..6342fa5392a 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -61,7 +61,7 @@ "display_pm_standard": { "name": "Display PM standard", "state": { - "ugm3": "µg/m³", + "ugm3": "μg/m³", "us_aqi": "US AQI" } }, diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 7025929abd8..dbabad96041 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -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, diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 8bdf448bfba..3d4c62ee1c7 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -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, diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index b5dee1deee3..be371837442 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -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.""" diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index da35975c193..68ee5739ab7 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -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, diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 3cb3959d3f2..2ca4e28a36d 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -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, diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 48327910be6..9fef970d560 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -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: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index ea67e30a3c1..9a0a288fad4 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -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 diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index c3cc31bf04f..9da68e62d80 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -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}") diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 83679894d71..3423fc161ce 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -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 ( diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 79ed56d2a75..1ebd35711ac 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -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 ): diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 02e11d1530a..76af35adeba 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -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, +} diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 3adc33e9935..ac0e8f249f5 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -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) diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index e14a165f81f..3952f76bddd 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -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" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 88f8dbbdaa2..56171707338 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -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 diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 92607ba07eb..e09923ad940 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -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, +} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c321caa616d..c20a3e2e1ae 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -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 diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 08e1991d831..f288f011061 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -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", diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 1ef18f4ea2b..7a80a51726d 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -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, diff --git a/homeassistant/const.py b/homeassistant/const.py index b74fa64d5c7..5ec3fb56903 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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" diff --git a/pylint/plugins/hass_enforce_greek_micro_char.py b/pylint/plugins/hass_enforce_greek_micro_char.py new file mode 100644 index 00000000000..909af66cd9e --- /dev/null +++ b/pylint/plugins/hass_enforce_greek_micro_char.py @@ -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)) diff --git a/pyproject.toml b/pyproject.toml index 343d581577b..4ed99327499 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index 575c596404b..e205e626ab8 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airgradient_raw_pm2_5', diff --git a/tests/components/airly/snapshots/test_sensor.ambr b/tests/components/airly/snapshots/test_sensor.ambr index efd809e76ae..8d79f8cdf0a 100644 --- a/tests/components/airly/snapshots/test_sensor.ambr +++ b/tests/components/airly/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_sulphur_dioxide', diff --git a/tests/components/airthings/snapshots/test_sensor.ambr b/tests/components/airthings/snapshots/test_sensor.ambr index 67a210ca037..9cc3d1bcd13 100644 --- a/tests/components/airthings/snapshots/test_sensor.ambr +++ b/tests/components/airthings/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.living_room_pm2_5', diff --git a/tests/components/altruist/snapshots/test_sensor.ambr b/tests/components/altruist/snapshots/test_sensor.ambr index ca74e75542f..9340e10cbe8 100644 --- a/tests/components/altruist/snapshots/test_sensor.ambr +++ b/tests/components/altruist/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.5366960e8b18_pm2_5', diff --git a/tests/components/arve/snapshots/test_sensor.ambr b/tests/components/arve/snapshots/test_sensor.ambr index eb51aa8c1f2..18643ac1755 100644 --- a/tests/components/arve/snapshots/test_sensor.ambr +++ b/tests/components/arve/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_sensor_pm10', @@ -330,7 +330,7 @@ 'device_class': 'pm25', 'friendly_name': 'Test Sensor PM2.5', 'state_class': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_sensor_pm2_5', diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index f1cffa8583f..63fdece9c98 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -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", }, diff --git a/tests/components/deconz/snapshots/test_sensor.ambr b/tests/components/deconz/snapshots/test_sensor.ambr index 04f93738b18..4a6bc43043b 100644 --- a/tests/components/deconz/snapshots/test_sensor.ambr +++ b/tests/components/deconz/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.airquality_1_pm25', diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 440df495995..5e2d9446cdc 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -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"] diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index abe90e72b56..005e6ec91d9 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -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( diff --git a/tests/components/gios/snapshots/test_sensor.ambr b/tests/components/gios/snapshots/test_sensor.ambr index 2a0afcc72b1..b7ad5b2d51d 100644 --- a/tests/components/gios/snapshots/test_sensor.ambr +++ b/tests/components/gios/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.home_sulphur_dioxide', diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index caed4a5c469..2410b5dbbde 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -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) diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 3b075b44356..95d24957fcb 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'entity_id': 'sensor.airversa_ap2_1808_pm2_5_density', 'state': '3.0', diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index 52fd6bb2ce4..377d29f4a71 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -94,7 +94,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_calibration_offset', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_state[number.pinecil_calibration_offset-state] @@ -105,7 +105,7 @@ 'min': 100, 'mode': , 'step': 1, - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.pinecil_calibration_offset', diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr index 39dda49d313..caab12d4120 100644 --- a/tests/components/iron_os/snapshots/test_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -566,7 +566,7 @@ 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.pinecil_raw_tip_voltage-state] @@ -575,7 +575,7 @@ 'device_class': 'voltage', 'friendly_name': 'Pinecil Raw tip voltage', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.pinecil_raw_tip_voltage', diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr index d561c4c6fc9..3f42d7e4f5c 100644 --- a/tests/components/lg_thinq/snapshots/test_sensor.ambr +++ b/tests/components/lg_thinq/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.test_air_conditioner_pm2_5', diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index eb34c7302e3..290016f0ff3 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_pm2_5', diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index fd54e5f0643..9d5dc8f0a8a 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -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", [ diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 16f0c9f22bc..f7198095aa2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -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", [ diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index cc6bc9bc7b6..3071752267e 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4', diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 4ccf8f69c42..b5e5e18f664 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -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.""" diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index de953861f80..ae80431f33c 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_sulphur_dioxide', diff --git a/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr b/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr index 2bdcb4f3a6a..2641297bc76 100644 --- a/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr +++ b/tests/components/ruuvitag_ble/snapshots/test_sensor.ambr @@ -776,7 +776,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '01:03:05:07:12:34-pm25', - 'unit_of_measurement': , + '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': , - 'unit_of_measurement': , + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.ruuvitag_884f_pm2_5', diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 62141186b55..ce78edfe481 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -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, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 43f185f939a..8b6d55cb9a9 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -3755,6 +3755,44 @@ async def test_compile_hourly_statistics_convert_units_1( 30, ), (None, "m3", "m³", 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", "m³", 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", "m³"), (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, "m³", "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( diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index dfc738bf7d7..7109b46cebb 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.corridor_a_c_pm2_5', diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 143520b68c2..e29255cdc72 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , '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': , - 'unit_of_measurement': 'µg/m³', + 'unit_of_measurement': 'μg/m³', }), 'context': , 'entity_id': 'sensor.air_purifier_600s_pm2_5', diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 3540c92682b..abfe140f6cb 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -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") diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index 8ae291ac0b7..4ffbca6124a 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -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 diff --git a/tests/pylint/test_enforce_greek_micro_char.py b/tests/pylint/test_enforce_greek_micro_char.py new file mode 100644 index 00000000000..fe0abd9af5f --- /dev/null +++ b/tests/pylint/test_enforce_greek_micro_char.py @@ -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" diff --git a/tests/test_const.py b/tests/test_const.py index f1ceaad6a08..3398a571f6f 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -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(