Fix use of ambiguous units for reactive power and energy (#161810)

This commit is contained in:
Jan Bouwhuis
2026-01-29 20:34:09 +01:00
committed by GitHub
parent 0fb62a7e97
commit 72e6ca55ba
4 changed files with 156 additions and 23 deletions
+7 -1
View File
@@ -594,7 +594,8 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
}
# We translate units that were using using the legacy coding of μ \u00b5
# to units using recommended coding of μ \u03bc
# to units using recommended coding of μ \u03bc and
# we convert alternative accepted units to the preferred unit.
AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
@@ -604,4 +605,9 @@ AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
"mVAr": UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE,
"VAr": UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
"kVAr": UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE,
"VArh": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR,
"kVArh": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR,
}
+7 -1
View File
@@ -840,7 +840,8 @@ STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]]
}
# We translate units that were using using the legacy coding of μ \u00b5
# to units using recommended coding of μ \u03bc
# to units using recommended coding of μ \u03bc and
# we convert alternative accepted units to the preferred unit.
AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
@@ -850,4 +851,9 @@ AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
"mVAr": UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE,
"VAr": UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
"kVAr": UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE,
"VArh": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR,
"kVArh": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR,
}
+107 -21
View File
@@ -2,6 +2,7 @@
import copy
from datetime import datetime, timedelta
from enum import StrEnum
import json
import logging
from pathlib import Path
@@ -20,6 +21,8 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfElectricPotential,
UnitOfReactiveEnergy,
UnitOfReactivePower,
UnitOfTemperature,
)
from homeassistant.core import Event, HomeAssistant, State, callback
@@ -909,24 +912,113 @@ async def test_invalid_unit_of_measurement(
@pytest.mark.parametrize(
"hass_config",
("hass_config", "device_class", "unit", "equivalent_unit"),
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "voltage",
"unit_of_measurement": "\u00b5V", # microVolt
pytest.param(
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "voltage",
"unit_of_measurement": "\u00b5V", # microVolt
}
}
}
}
},
"voltage",
UnitOfElectricPotential.MICROVOLT,
"\u00b5V",
id="microvolt",
),
pytest.param(
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "reactive_power",
"unit_of_measurement": "mVAr",
}
}
},
"reactive_power",
UnitOfReactivePower.MILLIVOLT_AMPERE_REACTIVE,
"mVAr",
id="mvar",
),
pytest.param(
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "reactive_power",
"unit_of_measurement": "VAr",
}
}
},
"reactive_power",
UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
"VAr",
id="var",
),
pytest.param(
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "reactive_power",
"unit_of_measurement": "kVAr",
}
}
},
"reactive_power",
UnitOfReactivePower.KILO_VOLT_AMPERE_REACTIVE,
"kVAr",
id="kvar",
),
pytest.param(
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "reactive_energy",
"unit_of_measurement": "VArh",
}
}
},
"reactive_energy",
UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR,
"VArh",
id="varh",
),
pytest.param(
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "reactive_energy",
"unit_of_measurement": "kVArh",
}
}
},
"reactive_energy",
UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR,
"kVArh",
id="kvarh",
),
],
)
async def test_device_class_with_equivalent_unit_of_measurement_received(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
device_class: str,
unit: StrEnum,
equivalent_unit: str,
) -> None:
"""Test device_class with equivalent unit of measurement."""
assert await mqtt_mock_entry()
@@ -935,20 +1027,17 @@ async def test_device_class_with_equivalent_unit_of_measurement_received(
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
)
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is unit
caplog.clear()
discovery_payload = {
"name": "bla",
"state_topic": "test-topic2",
"device_class": "voltage",
"unit_of_measurement": "\u00b5V",
"device_class": device_class,
"unit_of_measurement": equivalent_unit,
}
# Now discover a sensor with an altarantive mu char
# Now discover a sensor with an ambiguous unit
async_fire_mqtt_message(
hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload)
)
@@ -958,10 +1047,7 @@ async def test_device_class_with_equivalent_unit_of_measurement_received(
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
)
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is unit
@pytest.mark.parametrize(
+35
View File
@@ -3892,6 +3892,21 @@ async def test_compile_hourly_statistics_convert_units_1(
),
(None, "\u00b5g", "\u03bcg", None, "mass", 13.050847, 13.333333, -10, 30),
(None, "\u00b5s", "\u03bcs", None, "duration", 13.050847, 13.333333, -10, 30),
(None, "mVAr", "mvar", None, "reactive_power", 13.050847, 13.333333, -10, 30),
(None, "VAr", "var", None, "reactive_power", 13.050847, 13.333333, -10, 30),
(None, "kVAr", "kvar", None, "reactive_power", 13.050847, 13.333333, -10, 30),
(None, "VArh", "varh", None, "reactive_energy", 13.050847, 13.333333, -10, 30),
(
None,
"kVArh",
"kvarh",
None,
"reactive_energy",
13.050847,
13.333333,
-10,
30,
),
],
)
async def test_compile_hourly_statistics_equivalent_units_1(
@@ -4032,6 +4047,16 @@ async def test_compile_hourly_statistics_equivalent_units_1(
(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),
(None, "mVAr", "mvar", None, 13.333333, -10, 30),
(None, "VAr", "var", None, 13.333333, -10, 30),
(None, "kVAr", "kvar", None, 13.333333, -10, 30),
(None, "VArh", "varh", None, 13.333333, -10, 30),
(None, "kVArh", "kvarh", None, 13.333333, -10, 30),
(SensorDeviceClass.REACTIVE_POWER, "mVAr", "mvar", None, 13.333333, -10, 30),
(SensorDeviceClass.REACTIVE_POWER, "VAr", "var", None, 13.333333, -10, 30),
(SensorDeviceClass.REACTIVE_POWER, "kVAr", "kvar", None, 13.333333, -10, 30),
(SensorDeviceClass.REACTIVE_ENERGY, "VArh", "varh", None, 13.333333, -10, 30),
(SensorDeviceClass.REACTIVE_ENERGY, "kVArh", "kvarh", None, 13.333333, -10, 30),
],
)
async def test_compile_hourly_statistics_equivalent_units_2(
@@ -6008,6 +6033,11 @@ async def test_validate_statistics_unit_change_no_conversion(
(NONE_SENSOR_ATTRIBUTES, "\u00b5mol/s⋅m²", "\u03bcmol/s⋅m²"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5g", "\u03bcg"),
(NONE_SENSOR_ATTRIBUTES, "\u00b5s", "\u03bcs"),
(NONE_SENSOR_ATTRIBUTES, "mVAr", "mvar"),
(NONE_SENSOR_ATTRIBUTES, "VAr", "var"),
(NONE_SENSOR_ATTRIBUTES, "kVAr", "kvar"),
(NONE_SENSOR_ATTRIBUTES, "VArh", "varh"),
(NONE_SENSOR_ATTRIBUTES, "kVArh", "kvarh"),
],
)
async def test_validate_statistics_unit_change_equivalent_units(
@@ -6105,6 +6135,11 @@ async def test_validate_statistics_unit_change_equivalent_units(
"\u00b5s",
"d, h, min, ms, s, w, \u03bcs",
),
(NONE_SENSOR_ATTRIBUTES, "reactive_power", "mvar", "mVAr", "kvar, mvar, var"),
(NONE_SENSOR_ATTRIBUTES, "reactive_power", "var", "VAr", "kvar, mvar, var"),
(NONE_SENSOR_ATTRIBUTES, "reactive_power", "kvar", "kVAr", "kvar, mvar, var"),
(NONE_SENSOR_ATTRIBUTES, "reactive_energy", "varh", "VArh", "kvarh, varh"),
(NONE_SENSOR_ATTRIBUTES, "reactive_energy", "kvarh", "kVArh", "kvarh, varh"),
],
)
async def test_validate_statistics_unit_change_equivalent_units_2(