Deprecate use of invalid unit of measurement for mqtt sensor (#140164)

* Deprecate use of invalid unit of measurement for mqtt sensor

* Update learn more URL to point to user docs instead

* typo
This commit is contained in:
Jan Bouwhuis 2025-03-09 14:20:31 +01:00 committed by GitHub
parent e2d4e8b65d
commit 1a46edffaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 127 additions and 2 deletions

View File

@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant.components import sensor
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_UNITS,
DEVICE_CLASSES_SCHEMA,
ENTITY_ID_FORMAT,
STATE_CLASSES_SCHEMA,
@ -33,13 +34,14 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
from homeassistant.helpers.typing import ConfigType, VolSchemaType
from homeassistant.util import dt as dt_util
from . import subscription
from .config import MQTT_RO_SCHEMA
from .const import CONF_OPTIONS, CONF_STATE_TOPIC, PAYLOAD_NONE
from .const import CONF_OPTIONS, CONF_STATE_TOPIC, DOMAIN, PAYLOAD_NONE
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
@ -63,6 +65,10 @@ MQTT_SENSOR_ATTRIBUTES_BLOCKED = frozenset(
DEFAULT_NAME = "MQTT Sensor"
DEFAULT_FORCE_UPDATE = False
URL_DOCS_SUPPORTED_SENSOR_UOM = (
"https://www.home-assistant.io/integrations/sensor/#device-class"
)
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
{
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
@ -107,6 +113,23 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
)
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is None:
return config
if (
device_class in DEVICE_CLASS_UNITS
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
):
_LOGGER.warning(
"The unit of measurement `%s` is not valid "
"together with device class `%s`. "
"this will stop working in HA Core 2025.7.0",
unit_of_measurement,
device_class,
)
return config
@ -155,8 +178,40 @@ class MqttSensor(MqttEntity, RestoreSensor):
None
)
@callback
def async_check_uom(self) -> None:
"""Check if the unit of measurement is valid with the device class."""
if (
self._discovery_data is not None
or self.device_class is None
or self.native_unit_of_measurement is None
):
return
if (
self.device_class in DEVICE_CLASS_UNITS
and self.native_unit_of_measurement
not in DEVICE_CLASS_UNITS[self.device_class]
):
async_create_issue(
self.hass,
DOMAIN,
self.entity_id,
issue_domain=sensor.DOMAIN,
is_fixable=False,
severity=IssueSeverity.WARNING,
learn_more_url=URL_DOCS_SUPPORTED_SENSOR_UOM,
translation_placeholders={
"uom": self.native_unit_of_measurement,
"device_class": self.device_class.value,
"entity_id": self.entity_id,
},
translation_key="invalid_unit_of_measurement",
breaks_in_ha_version="2025.7.0",
)
async def mqtt_async_added_to_hass(self) -> None:
"""Restore state for entities with expire_after set."""
self.async_check_uom()
last_state: State | None
last_sensor_data: SensorExtraStoredData | None
if (

View File

@ -3,6 +3,10 @@
"invalid_platform_config": {
"title": "Invalid config found for mqtt {domain} item",
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
},
"invalid_unit_of_measurement": {
"title": "Sensor with invalid unit of measurement",
"description": "Manual configured Sensor entity **{entity_id}** has a configured unit of measurement **{uom}** which is not valid with configured device class **{device_class}**. Make sure a valid unit of measurement is configured or remove the device class, and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
}
},
"config": {

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@ -71,6 +71,7 @@ from .test_common import (
from tests.common import (
MockConfigEntry,
async_capture_events,
async_fire_mqtt_message,
async_fire_time_changed,
mock_restore_cache_with_extra_data,
@ -870,6 +871,71 @@ async def test_invalid_device_class(
assert "expected SensorDeviceClass or one of" in caplog.text
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"device_class": "energy",
"unit_of_measurement": "ppm",
}
}
}
],
)
async def test_invalid_unit_of_measurement(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device_class with invalid unit of measurement."""
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
assert await mqtt_mock_entry()
assert (
"The unit of measurement `ppm` is not valid together with device class `energy`"
in caplog.text
)
# A repair issue was logged
assert len(events) == 1
assert events[0].data["issue_id"] == "sensor.test"
# Assert the sensor works
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"
caplog.clear()
discovery_payload = {
"name": "bla",
"state_topic": "test-topic2",
"device_class": "temperature",
"unit_of_measurement": "C",
}
# Now discover an other invalid sensor
async_fire_mqtt_message(
hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload)
)
await hass.async_block_till_done()
assert (
"The unit of measurement `C` is not valid together with device class `temperature`"
in caplog.text
)
# Assert the sensor works
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"
# No new issue was registered for the discovered entity
assert len(events) == 1
@pytest.mark.parametrize(
"hass_config",
[