diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index c3cc31bf04f..feaa0189108 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.recorder import EQUIVALENT_UNITS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -70,6 +71,13 @@ 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 EQUIVALENT_UNITS + ): + config[CONF_UNIT_OF_MEASUREMENT] = EQUIVALENT_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 b48a3c37a81..d7ac3d9474a 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -130,13 +130,15 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT if (device_class := config.get(CONF_DEVICE_CLASS)) is None or ( unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) - ) is None: + ) in EQUIVALENT_UNITS: + unit_of_measurement = EQUIVALENT_UNITS[unit_of_measurement] + config[CONF_UNIT_OF_MEASUREMENT] = unit_of_measurement + + if ( + device_class := config.get(CONF_DEVICE_CLASS) + ) is None or unit_of_measurement is None: return config - if unit_of_measurement in EQUIVALENT_UNITS: - unit_of_measurement = config[CONF_UNIT_OF_MEASUREMENT] = EQUIVALENT_UNITS[ - unit_of_measurement - ] if ( device_class in DEVICE_CLASS_UNITS and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class] diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 6c1ff110392..1f8caf75667 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -91,7 +91,7 @@ EQUIVALENT_UNITS = { "\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, "\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light - "\u0065g": UnitOfMass.MICROGRAMS, + "\u00b5g": UnitOfMass.MICROGRAMS, "\u00b5s": UnitOfTime.MICROSECONDS, } 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 ffe93851e48..a42df8f3811 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 @@ -933,6 +935,10 @@ 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 + ) caplog.clear() @@ -952,6 +958,10 @@ 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 + ) @pytest.mark.parametrize( @@ -980,6 +990,10 @@ async def test_equivalent_unit_of_measurement_received_without_device_class( 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() @@ -998,6 +1012,10 @@ async def test_equivalent_unit_of_measurement_received_without_device_class( 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(