From 4aaafb0a991e96fe1e4c8dee29f3ab12c92a3b72 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Mar 2022 15:38:33 +0100 Subject: [PATCH 01/12] Fix false positive MQTT climate deprecation warnings for defaults (#67661) Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/climate.py | 32 +++++-- tests/components/mqtt/test_climate.py | 113 +++++++++++++++++++++++ 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index e145edde7d7..94320cc5def 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -271,7 +271,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list, + vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -298,7 +298,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( ), vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, + vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together @@ -431,6 +431,12 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + self._send_if_off = True + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + self._hold_list = [] + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -499,6 +505,15 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + if CONF_SEND_IF_OFF in config: + self._send_if_off = config[CONF_SEND_IF_OFF] + + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + if CONF_HOLD_LIST in config: + self._hold_list = config[CONF_HOLD_LIST] + def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -806,7 +821,9 @@ class MqttClimate(MqttEntity, ClimateEntity): ): presets.append(PRESET_AWAY) - presets.extend(self._config[CONF_HOLD_LIST]) + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + presets.extend(self._hold_list) if presets: presets.insert(0, PRESET_NONE) @@ -847,10 +864,7 @@ class MqttClimate(MqttEntity, ClimateEntity): setattr(self, attr, temp) # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if ( - self._config[CONF_SEND_IF_OFF] - or self._current_operation != HVAC_MODE_OFF - ): + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) @@ -890,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( swing_mode ) @@ -903,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c3501267e12..93249e76875 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -333,6 +333,43 @@ async def test_set_fan_mode(hass, mqtt_mock): assert state.attributes.get("fan_mode") == "high" +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("fan-mode-topic", "low", 0, False)]), + ({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_fan_mode_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of fan mode if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for fan_mode should be sent when the device is turned on + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -385,6 +422,43 @@ async def test_set_swing(hass, mqtt_mock): assert state.attributes.get("swing_mode") == "on" +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("swing-mode-topic", "on", 0, False)]), + ({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_swing_mode_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of swing mode if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for swing_mode should be sent when the device is turned on + await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_target_temperature(hass, mqtt_mock): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -421,6 +495,45 @@ async def test_set_target_temperature(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("temperature-topic", "21.0", 0, False)]), + ({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_target_temperature_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of target temperature if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for target temperature should be sent when the device is turned on + await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "temperature-topic", "16.0", 0, False + ) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for target temperature sent should be if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_target_temperature_pessimistic(hass, mqtt_mock): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) From 87492e6b3e97f4211e3187ff9f73343f9311d802 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Mon, 7 Mar 2022 09:14:05 +0000 Subject: [PATCH 02/12] Fix timezone for growatt lastdataupdate (#67684) * Added timezone for growatt lastdataupdate (#67646) * Growatt lastdataupdate set to local timezone --- homeassistant/components/growatt_server/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 67095492de7..db045242987 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -222,7 +222,7 @@ class GrowattData: date_now = dt.now().date() last_updated_time = dt.parse_time(str(sorted_keys[-1])) mix_detail["lastdataupdate"] = datetime.datetime.combine( - date_now, last_updated_time + date_now, last_updated_time, dt.DEFAULT_TIME_ZONE ) # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined From 814c96834efd2ce108eea7df696a1e2030ab2a41 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Mar 2022 18:55:12 +0100 Subject: [PATCH 03/12] Fix temperature stepping in Sensibo (#67737) Co-authored-by: Paulus Schoutsen --- homeassistant/components/sensibo/coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index ef0475640b5..a76654e3c68 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -15,6 +15,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT +MAX_POSSIBLE_STEP = 1000 + class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" @@ -74,7 +76,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): .get("values", [0, 1]) ) if temperatures_list: - temperature_step = temperatures_list[1] - temperatures_list[0] + diff = MAX_POSSIBLE_STEP + for i in range(len(temperatures_list) - 1): + if temperatures_list[i + 1] - temperatures_list[i] < diff: + diff = temperatures_list[i + 1] - temperatures_list[i] + temperature_step = diff active_features = list(ac_states) full_features = set() From f4ec7e0902b4eef30e92fa84eb718d09e3d7fa11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 05:42:16 +0100 Subject: [PATCH 04/12] Prevent polling from recreating an entity after removal (#67750) --- homeassistant/helpers/entity.py | 30 +++++++++++++++++++++------ tests/helpers/test_entity.py | 16 ++++++++++++++ tests/helpers/test_entity_platform.py | 24 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8e4f6bc8b58..a554a093c5c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Awaitable, Iterable, Mapping, MutableMapping from dataclasses import dataclass from datetime import datetime, timedelta +from enum import Enum, auto import functools as ft import logging import math @@ -207,6 +208,19 @@ class EntityCategory(StrEnum): SYSTEM = "system" +class EntityPlatformState(Enum): + """The platform state of an entity.""" + + # Not Added: Not yet added to a platform, polling updates are written to the state machine + NOT_ADDED = auto() + + # Added: Added to a platform, polling updates are written to the state machine + ADDED = auto() + + # Removed: Removed from a platform, polling updates are not written to the state machine + REMOVED = auto() + + def convert_to_entity_category( value: EntityCategory | str | None, raise_report: bool = True ) -> EntityCategory | None: @@ -294,7 +308,7 @@ class Entity(ABC): _context_set: datetime | None = None # If entity is added to an entity platform - _added = False + _platform_state = EntityPlatformState.NOT_ADDED # Entity Properties _attr_assumed_state: bool = False @@ -553,6 +567,10 @@ class Entity(ABC): @callback def _async_write_ha_state(self) -> None: """Write the state to the state machine.""" + if self._platform_state == EntityPlatformState.REMOVED: + # Polling returned after the entity has already been removed + return + if self.registry_entry and self.registry_entry.disabled_by: if not self._disabled_reported: self._disabled_reported = True @@ -758,7 +776,7 @@ class Entity(ABC): parallel_updates: asyncio.Semaphore | None, ) -> None: """Start adding an entity to a platform.""" - if self._added: + if self._platform_state == EntityPlatformState.ADDED: raise HomeAssistantError( f"Entity {self.entity_id} cannot be added a second time to an entity platform" ) @@ -766,7 +784,7 @@ class Entity(ABC): self.hass = hass self.platform = platform self.parallel_updates = parallel_updates - self._added = True + self._platform_state = EntityPlatformState.ADDED @callback def add_to_platform_abort(self) -> None: @@ -774,7 +792,7 @@ class Entity(ABC): self.hass = None # type: ignore[assignment] self.platform = None self.parallel_updates = None - self._added = False + self._platform_state = EntityPlatformState.NOT_ADDED async def add_to_platform_finish(self) -> None: """Finish adding an entity to a platform.""" @@ -792,12 +810,12 @@ class Entity(ABC): If the entity doesn't have a non disabled entry in the entity registry, or if force_remove=True, its state will be removed. """ - if self.platform and not self._added: + if self.platform and self._platform_state != EntityPlatformState.ADDED: raise HomeAssistantError( f"Entity {self.entity_id} async_remove called twice" ) - self._added = False + self._platform_state = EntityPlatformState.REMOVED if self._on_remove is not None: while self._on_remove: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 6b7de074a24..afc0887371e 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -545,6 +545,22 @@ async def test_async_remove_runs_callbacks(hass): assert len(result) == 1 +async def test_async_remove_ignores_in_flight_polling(hass): + """Test in flight polling is ignored after removing.""" + result = [] + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "test.test" + ent.async_on_remove(lambda: result.append(1)) + ent.async_write_ha_state() + assert hass.states.get("test.test").state == STATE_UNKNOWN + await ent.async_remove() + assert len(result) == 1 + assert hass.states.get("test.test") is None + ent.async_write_ha_state() + + async def test_set_context(hass): """Test setting context.""" context = Context() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9aa0a849e5a..c98fdff7858 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -390,6 +390,30 @@ async def test_async_remove_with_platform(hass): assert len(hass.states.async_entity_ids()) == 0 +async def test_async_remove_with_platform_update_finishes(hass): + """Remove an entity when an update finishes after its been removed.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name="test_1") + + async def _delayed_update(*args, **kwargs): + await asyncio.sleep(0.01) + + entity1.async_update = _delayed_update + + # Add, remove, add, remove and make sure no updates + # cause the entity to reappear after removal + for i in range(2): + await component.async_add_entities([entity1]) + assert len(hass.states.async_entity_ids()) == 1 + entity1.async_write_ha_state() + assert hass.states.get(entity1.entity_id) is not None + task = asyncio.create_task(entity1.async_update_ha_state(True)) + await entity1.async_remove() + assert len(hass.states.async_entity_ids()) == 0 + await task + assert len(hass.states.async_entity_ids()) == 0 + + async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): """Test for not adding duplicate entities.""" caplog.set_level(logging.ERROR) From c807c57a9bf3693fe0d1e72368b86e4253e87888 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Mar 2022 17:23:08 +0100 Subject: [PATCH 05/12] Fix internet access switch for old discovery (#67777) --- homeassistant/components/fritz/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 4c307c126cd..21039d45afa 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): ) self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): + if info.ip_address: + info.wan_access = self._get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True self.send_signal_device_update(new_device) From dfa1c3abb351e17565120522da385d56bbf8a7ae Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Mar 2022 18:05:10 +0100 Subject: [PATCH 06/12] Fix profile name update for Shelly Valve (#67778) --- homeassistant/components/shelly/climate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 2c81ecbe183..1e7ba2dd183 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -317,4 +317,14 @@ class BlockSleepingClimate( if self.device_block and self.block: _LOGGER.debug("Entity %s attached to blocks", self.name) + + assert self.block.channel + + self._preset_modes = [ + PRESET_NONE, + *self.wrapper.device.settings["thermostats"][int(self.block.channel)][ + "schedule_profile_names" + ], + ] + self.async_write_ha_state() From 8d7cdceb75482e2b6a68a20d714073e92acb6ea0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Mar 2022 12:10:38 -0500 Subject: [PATCH 07/12] Handle fan_modes being set to None in homekit (#67790) --- .../components/homekit/type_thermostats.py | 25 ++-- .../homekit/test_type_thermostats.py | 127 ++++++++++++++++++ 2 files changed, 139 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 8c54896e85e..1e20d1bc710 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -285,20 +285,19 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_HUMIDITY, value=50 ) - fan_modes = self.fan_modes = { - fan_mode.lower(): fan_mode - for fan_mode in attributes.get(ATTR_FAN_MODES, []) - } + fan_modes = {} self.ordered_fan_speeds = [] - if ( - features & SUPPORT_FAN_MODE - and fan_modes - and PRE_DEFINED_FAN_MODES.intersection(fan_modes) - ): - self.ordered_fan_speeds = [ - speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes - ] - self.fan_chars.append(CHAR_ROTATION_SPEED) + + if features & SUPPORT_FAN_MODE: + fan_modes = { + fan_mode.lower(): fan_mode + for fan_mode in attributes.get(ATTR_FAN_MODES) or [] + } + if fan_modes and PRE_DEFINED_FAN_MODES.intersection(fan_modes): + self.ordered_fan_speeds = [ + speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes + ] + self.fan_chars.append(CHAR_ROTATION_SPEED) if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds): self.fan_chars.append(CHAR_TARGET_FAN_STATE) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d1db618e7e4..5f002fbbf6c 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -15,6 +15,8 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, @@ -74,6 +76,7 @@ from homeassistant.components.homekit.type_thermostats import ( from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, @@ -2349,3 +2352,127 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): assert len(call_set_fan_mode) == 2 assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF + + +async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events): + """Test a thermostate with fan modes set to None.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: None, + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + + +async def test_thermostat_with_fan_modes_set_to_none_not_supported( + hass, hk_driver, events +): + """Test a thermostate with fan modes set to None and supported feature missing.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: None, + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + + +async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( + hass, hk_driver, events +): + """Test a thermostate with fan mode and supported feature missing.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, + ATTR_MIN_TEMP: 44.6, + ATTR_MAX_TEMP: 95, + ATTR_PRESET_MODES: ["home", "away"], + ATTR_TEMPERATURE: 67, + ATTR_TARGET_TEMP_HIGH: None, + ATTR_TARGET_TEMP_LOW: None, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_FAN_MODES: None, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_PRESET_MODE: "home", + ATTR_FRIENDLY_NAME: "Rec Room", + ATTR_HVAC_MODES: [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.ordered_fan_speeds == [] + assert not acc.fan_chars From 97ba17d1eca406cd06dfe35d72d437e63e9fa2f2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Mar 2022 18:14:14 +0100 Subject: [PATCH 08/12] Catch Elgato connection errors (#67799) --- homeassistant/components/elgato/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 805a70613f9..f15ccc0a03d 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -1,13 +1,13 @@ """Support for Elgato Lights.""" from typing import NamedTuple -from elgato import Elgato, Info, State +from elgato import Elgato, ElgatoConnectionError, Info, State from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER, SCAN_INTERVAL @@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) + async def _async_update_data() -> State: + """Fetch Elgato data.""" + try: + return await elgato.state() + except ElgatoConnectionError as err: + raise UpdateFailed(err) from err + coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator( hass, LOGGER, name=f"{DOMAIN}_{entry.data[CONF_HOST]}", update_interval=SCAN_INTERVAL, - update_method=elgato.state, + update_method=_async_update_data, ) await coordinator.async_config_entry_first_refresh() From 580c998552b3e9578c2db418b923ae63c9315df7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 17:56:52 +0100 Subject: [PATCH 09/12] Update frontend to 20220301.1 (#67812) --- homeassistant/components/frontend/manifest.json | 5 +++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cc118e23dc9..baf61343040 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220301.0" + "home-assistant-frontend==20220301.1" ], "dependencies": [ "api", @@ -13,7 +13,8 @@ "diagnostics", "http", "lovelace", - "onboarding", "search", + "onboarding", + "search", "system_log", "websocket_api" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bd9e1da6a69..2a441dffc35 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2ec33b35621..53849ece349 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -843,7 +843,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 070b4e3b10b..d0f8c9513a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 4e6fc3615b16568caeb1b47bb387b6526a9fbfc7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 8 Mar 2022 00:43:05 +0100 Subject: [PATCH 10/12] Bump python-miio version to 0.5.11 (#67824) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 0091d58e1e2..7157e32299a 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.10"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 53849ece349..e74e0915572 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1952,7 +1952,7 @@ python-kasa==0.4.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.10 +python-miio==0.5.11 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0f8c9513a0..2031915fc1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1219,7 +1219,7 @@ python-juicenet==1.0.2 python-kasa==0.4.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.10 +python-miio==0.5.11 # homeassistant.components.nest python-nest==4.2.0 From b09ab2dafbfb3a78d7214b6a39c56fcf3856d63e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 05:43:19 +0100 Subject: [PATCH 11/12] Prevent scene from restoring unavailable states (#67836) --- homeassistant/components/scene/__init__.py | 8 ++++-- tests/components/scene/test_init.py | 29 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 846c0fbc7c6..5dea5965d43 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ATTR_TRANSITION from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON +from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -117,7 +117,11 @@ class Scene(RestoreEntity): """Call when the scene is added to hass.""" await super().async_internal_added_to_hass() state = await self.async_get_last_state() - if state is not None and state.state is not None: + if ( + state is not None + and state.state is not None + and state.state != STATE_UNAVAILABLE + ): self.__last_activated = state.state def activate(self, **kwargs: Any) -> None: diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 41b16261cd1..3dd0cfce7b9 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State @@ -177,6 +178,34 @@ async def test_restore_state(hass, entities, enable_custom_integrations): assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00" +async def test_restore_state_does_not_restore_unavailable( + hass, entities, enable_custom_integrations +): + """Test we restore state integration but ignore unavailable.""" + mock_restore_cache(hass, (State("scene.test", STATE_UNAVAILABLE),)) + + light_1, light_2 = await setup_lights(hass, entities) + + assert await async_setup_component( + hass, + scene.DOMAIN, + { + "scene": [ + { + "name": "test", + "entities": { + light_1.entity_id: "on", + light_2.entity_id: "on", + }, + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("scene.test").state == STATE_UNKNOWN + + async def activate(hass, entity_id=ENTITY_MATCH_ALL): """Activate a scene.""" data = {} From a1abcbc7ebf386dc52c3c2292b74eb38e0dfad38 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2022 20:45:49 -0800 Subject: [PATCH 12/12] Bumped version to 2022.3.3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e91d5cf3725..c1d791aaf07 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index e9a6dbb8ee5..a3c8068edb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.2 +version = 2022.3.3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0