From e36fd5f222887e78eb5df983291e8c30ccb28f64 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 12 Apr 2023 19:14:16 +0200 Subject: [PATCH] Allow None device_class and UOM for mqtt entities (#91240) * Allow None device_class and UOM for mqtt entities * Rever not needed changes * Revert another unwanted change --- homeassistant/components/mqtt/humidifier.py | 2 +- homeassistant/components/mqtt/number.py | 2 +- homeassistant/components/mqtt/sensor.py | 2 +- tests/components/mqtt/test_binary_sensor.py | 41 ++++++--- tests/components/mqtt/test_button.py | 5 ++ tests/components/mqtt/test_humidifier.py | 13 +++ tests/components/mqtt/test_number.py | 96 ++++++++++++++------- tests/components/mqtt/test_sensor.py | 5 +- tests/components/mqtt/test_switch.py | 48 ++++++++--- tests/components/mqtt/test_update.py | 52 ++++++++--- 10 files changed, 190 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 93069791a79..2c6dae54f4c 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -125,7 +125,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional( CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER ): vol.In( - [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] + [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER, None] ), vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 62bb9123a76..1ab14b2b4f8 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -97,7 +97,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) ), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.Any(cv.string, None), vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index aea357bea62..9de442926a0 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -107,7 +107,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SUGGESTED_DISPLAY_PRECISION): cv.positive_int, vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.Any(cv.string, None), } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 0b374df07f4..c98de3628b8 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -505,27 +505,44 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "device_class"), [ - { - mqtt.DOMAIN: { - binary_sensor.DOMAIN: { - "name": "test", - "device_class": "motion", - "state_topic": "test-topic", + ( + { + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "device_class": "motion", + "state_topic": "test-topic", + } } - } - } + }, + "motion", + ), + ( + { + mqtt.DOMAIN: { + binary_sensor.DOMAIN: { + "name": "test", + "device_class": None, + "state_topic": "test-topic", + } + } + }, + None, + ), ], ) async def test_valid_device_class( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + device_class: str | None, ) -> None: - """Test the setting of a valid sensor class.""" + """Test the setting of a valid sensor class and ignoring an empty device_class.""" await mqtt_mock_entry() state = hass.states.get("binary_sensor.test") - assert state.attributes.get("device_class") == "motion" + assert state.attributes.get("device_class") == device_class @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 9a21fed727b..e99182323c8 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -477,6 +477,11 @@ async def test_invalid_device_class( "name": "Test 3", "command_topic": "test-topic", }, + { + "name": "Test 4", + "command_topic": "test-topic", + "device_class": None, + }, ] } } diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 8cf0c5f47d8..90b2e6d5ba6 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -867,6 +867,19 @@ async def test_attributes( }, True, ), + ( + { + mqtt.DOMAIN: { + humidifier.DOMAIN: { + "name": "test_valid_4", + "command_topic": "command-topic", + "target_humidity_command_topic": "humidity-command-topic", + "device_class": None, + } + } + }, + True, + ), ( { mqtt.DOMAIN: { diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 1c47793e342..f12f5eca8b6 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -18,7 +18,6 @@ from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, - NumberDeviceClass, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -76,45 +75,80 @@ def number_platform_only(): @pytest.mark.parametrize( - "hass_config", + ("hass_config", "device_class", "unit_of_measurement", "values"), [ - { - mqtt.DOMAIN: { - number.DOMAIN: { - "state_topic": "test/state_number", - "command_topic": "test/cmd_number", - "name": "Test Number", - "device_class": "temperature", - "unit_of_measurement": UnitOfTemperature.FAHRENHEIT.value, - "payload_reset": "reset!", + ( + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": "test/state_number", + "command_topic": "test/cmd_number", + "name": "Test Number", + "device_class": "temperature", + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT.value, + "payload_reset": "reset!", + } } - } - } + }, + "temperature", + UnitOfTemperature.CELSIUS.value, + [("10", "-12.0"), ("20.5", "-6.4")], # 10 °F -> -12 °C + ), + ( + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": "test/state_number", + "command_topic": "test/cmd_number", + "name": "Test Number", + "device_class": "temperature", + "unit_of_measurement": UnitOfTemperature.CELSIUS.value, + "payload_reset": "reset!", + } + } + }, + "temperature", + UnitOfTemperature.CELSIUS.value, + [("10", "10"), ("15", "15")], + ), + ( + { + mqtt.DOMAIN: { + number.DOMAIN: { + "state_topic": "test/state_number", + "command_topic": "test/cmd_number", + "name": "Test Number", + "device_class": None, + "unit_of_measurement": None, + "payload_reset": "reset!", + } + } + }, + None, + None, + [("10", "10"), ("20", "20")], + ), ], ) async def test_run_number_setup( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + device_class: str | None, + unit_of_measurement: UnitOfTemperature | None, + values: list[tuple[str, str]], ) -> None: """Test that it fetches the given payload.""" await mqtt_mock_entry() - async_fire_mqtt_message(hass, "test/state_number", "10") + for payload, value in values: + async_fire_mqtt_message(hass, "test/state_number", payload) - await hass.async_block_till_done() + await hass.async_block_till_done() - state = hass.states.get("number.test_number") - assert state.state == "-12.0" # 10 °F -> -12 °C - assert state.attributes.get(ATTR_DEVICE_CLASS) == NumberDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" - - async_fire_mqtt_message(hass, "test/state_number", "20.5") - - await hass.async_block_till_done() - - state = hass.states.get("number.test_number") - assert state.state == "-6.4" # 20.5 °F -> -6.4 °C - assert state.attributes.get(ATTR_DEVICE_CLASS) == NumberDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" + state = hass.states.get("number.test_number") + assert state.state == value + assert state.attributes.get(ATTR_DEVICE_CLASS) == device_class + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement async_fire_mqtt_message(hass, "test/state_number", "reset!") @@ -122,8 +156,8 @@ async def test_run_number_setup( state = hass.states.get("number.test_number") assert state.state == "unknown" - assert state.attributes.get(ATTR_DEVICE_CLASS) == NumberDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" + assert state.attributes.get(ATTR_DEVICE_CLASS) == device_class + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 67979e69e1d..64499f11140 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -851,16 +851,17 @@ async def test_invalid_device_class( "name": "Test 3", "state_topic": "test-topic", "device_class": None, + "unit_of_measurement": None, }, ] } } ], ) -async def test_valid_device_class( +async def test_valid_device_class_and_uom( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: - """Test device_class option with valid values.""" + """Test device_class option with valid values and test with an empty unit of measurement.""" await mqtt_mock_entry() state = hass.states.get("sensor.test_1") diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 5088558b143..b06cfa34442 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -62,31 +62,51 @@ def switch_platform_only(): @pytest.mark.parametrize( - "hass_config", + ("hass_config", "device_class"), [ - { - mqtt.DOMAIN: { - switch.DOMAIN: { - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "payload_on": 1, - "payload_off": 0, - "device_class": "switch", + ( + { + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "device_class": "switch", + } } - } - } + }, + "switch", + ), + ( + { + mqtt.DOMAIN: { + switch.DOMAIN: { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + "device_class": None, + } + } + }, + None, + ), ], ) async def test_controlling_state_via_topic( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + device_class: str | None, ) -> None: """Test the controlling state via topic.""" await mqtt_mock_entry() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_DEVICE_CLASS) == "switch" + assert state.attributes.get(ATTR_DEVICE_CLASS) == device_class assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "state-topic", "1") diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index e3f06d88f9d..8e2cdaf8eaa 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -63,25 +63,48 @@ def update_platform_only(): @pytest.mark.parametrize( - "hass_config", + ("hass_config", "device_class"), [ - { - mqtt.DOMAIN: { - update.DOMAIN: { - "state_topic": "test/installed-version", - "latest_version_topic": "test/latest-version", - "name": "Test Update", - "release_summary": "Test release summary", - "release_url": "https://example.com/release", - "title": "Test Update Title", - "entity_picture": "https://example.com/icon.png", + ( + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": "test/installed-version", + "latest_version_topic": "test/latest-version", + "name": "Test Update", + "release_summary": "Test release summary", + "release_url": "https://example.com/release", + "title": "Test Update Title", + "entity_picture": "https://example.com/icon.png", + "device_class": "firmware", + } } - } - } + }, + "firmware", + ), + ( + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": "test/installed-version", + "latest_version_topic": "test/latest-version", + "name": "Test Update", + "release_summary": "Test release summary", + "release_url": "https://example.com/release", + "title": "Test Update Title", + "entity_picture": "https://example.com/icon.png", + "device_class": None, + } + } + }, + None, + ), ], ) async def test_run_update_setup( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + device_class: str | None, ) -> None: """Test that it fetches the given payload.""" installed_version_topic = "test/installed-version" @@ -101,6 +124,7 @@ async def test_run_update_setup( assert state.attributes.get("release_url") == "https://example.com/release" assert state.attributes.get("title") == "Test Update Title" assert state.attributes.get("entity_picture") == "https://example.com/icon.png" + assert state.attributes.get("device_class") == device_class async_fire_mqtt_message(hass, latest_version_topic, "2.0.0")