diff --git a/homeassistant/components/ecovacs/image.py b/homeassistant/components/ecovacs/image.py index b1c2f0075f12dd..5fa00fc5e434a0 100644 --- a/homeassistant/components/ecovacs/image.py +++ b/homeassistant/components/ecovacs/image.py @@ -69,7 +69,9 @@ async def async_added_to_hass(self) -> None: await super().async_added_to_hass() async def on_info(event: CachedMapInfoEvent) -> None: - self._attr_extra_state_attributes["map_name"] = event.name + for map_obj in event.maps: + if map_obj.using: + self._attr_extra_state_attributes["map_name"] = map_obj.name async def on_changed(event: MapChangedEvent) -> None: self._attr_image_last_updated = event.when diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 3495126fd15f7d..8d57eda6f4cad8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.11", "deebot-client==14.0.0"] + "requirements": ["py-sucks==0.9.11", "deebot-client==15.0.0"] } diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 8e72457f4a7b08..85b21da1dd515c 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -3,14 +3,15 @@ from __future__ import annotations from datetime import timedelta +from enum import IntEnum import logging from typing import Any from pyephember2.pyephember2 import ( EphEmber, ZoneMode, + boiler_state, zone_current_temperature, - zone_is_active, zone_is_hotwater, zone_mode, zone_name, @@ -53,6 +54,15 @@ "OFF": HVACMode.OFF, } + +class EPHBoilerStates(IntEnum): + """Boiler states for a zone given by the api.""" + + FIXME = 0 + OFF = 1 + ON = 2 + + HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()} @@ -123,7 +133,7 @@ def target_temperature(self) -> float | None: @property def hvac_action(self) -> HVACAction: """Return current HVAC action.""" - if zone_is_active(self._zone): + if boiler_state(self._zone) == EPHBoilerStates.ON: return HVACAction.HEATING return HVACAction.IDLE diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index d907f863988363..3da3d6cf06a0da 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -108,6 +108,7 @@ def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_HUMIDIFIER: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( # We use async_handle_source_entity_changes to track changes to the humidifer, @@ -140,6 +141,7 @@ async def async_sensor_updated( entry, options={**entry.options, CONF_SENSOR: data["entity_id"]}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( async_track_entity_registry_updated_event( @@ -148,7 +150,6 @@ async def async_sensor_updated( ) await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True @@ -186,11 +187,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/generic_hygrostat/config_flow.py b/homeassistant/components/generic_hygrostat/config_flow.py index 449fa49b713861..88cf12d741be24 100644 --- a/homeassistant/components/generic_hygrostat/config_flow.py +++ b/homeassistant/components/generic_hygrostat/config_flow.py @@ -96,6 +96,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/local_file/__init__.py b/homeassistant/components/local_file/__init__.py index 70144cd070477d..183c8b7ee82ea5 100644 --- a/homeassistant/components/local_file/__init__.py +++ b/homeassistant/components/local_file/__init__.py @@ -22,7 +22,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -30,8 +29,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Local file config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/local_file/config_flow.py b/homeassistant/components/local_file/config_flow.py index c4b83f9407af8c..206e4c2a7c83ad 100644 --- a/homeassistant/components/local_file/config_flow.py +++ b/homeassistant/components/local_file/config_flow.py @@ -65,6 +65,7 @@ class LocalFileConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 2776feba2724be..98620d957d289f 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -56,7 +56,6 @@ async def async_setup_entry( entry.runtime_data = SystemMonitorData(coordinator, psutil_wrapper) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -67,11 +66,6 @@ async def async_unload_entry( return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def update_listener(hass: HomeAssistant, entry: SystemMonitorConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_migrate_entry( hass: HomeAssistant, entry: SystemMonitorConfigEntry ) -> bool: diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index 4be31f6944caa7..66c4913f19e583 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -92,6 +92,8 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True + VERSION = 1 MINOR_VERSION = 3 diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index 4f3f365ea59e40..3740c6b685f3dc 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -13,16 +13,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, (Platform.BINARY_SENSOR,) ) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) - return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py index 0bbd5a528afe9e..df9596f3a20c7c 100644 --- a/homeassistant/components/tod/config_flow.py +++ b/homeassistant/components/tod/config_flow.py @@ -43,6 +43,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index 332ec9455eb9dc..c274744a630636 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -36,6 +36,7 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_ENTITY_ID: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) async def source_entity_removed() -> None: # The source entity has been removed, we remove the config entry because @@ -57,7 +58,6 @@ async def source_entity_removed() -> None: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True @@ -96,11 +96,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle an Trend options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/trend/config_flow.py b/homeassistant/components/trend/config_flow.py index 3bb06ae30420dd..d8c2f1ba1a91c6 100644 --- a/homeassistant/components/trend/config_flow.py +++ b/homeassistant/components/trend/config_flow.py @@ -110,6 +110,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): options_flow = { "init": SchemaFlowFormStep(get_extended_options_schema), } + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/workday/calendar.py b/homeassistant/components/workday/calendar.py index b6c7893b142d17..82f2942d1f9335 100644 --- a/homeassistant/components/workday/calendar.py +++ b/homeassistant/components/workday/calendar.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from holidays import HolidayBase @@ -15,8 +15,6 @@ from .const import CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS from .entity import BaseWorkdayEntity -CALENDAR_DAYS_AHEAD = 365 - async def async_setup_entry( hass: HomeAssistant, @@ -73,8 +71,10 @@ def __init__( def update_data(self, now: datetime) -> None: """Update data.""" event_list = [] - for i in range(CALENDAR_DAYS_AHEAD): - future_date = now.date() + timedelta(days=i) + start_date = date(now.year, 1, 1) + end_number_of_days = date(now.year + 1, 12, 31) - start_date + for i in range(end_number_of_days.days + 1): + future_date = start_date + timedelta(days=i) if self.date_is_workday(future_date): event = CalendarEvent( summary=self._name, diff --git a/requirements_all.txt b/requirements_all.txt index af5fb962a20993..21e30ff1bee66a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ decora-wifi==1.4 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==14.0.0 +deebot-client==15.0.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 601cf49af58ecb..850870baefe9f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -682,7 +682,7 @@ debugpy==1.8.16 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==14.0.0 +deebot-client==15.0.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 32c7558530c176..2e46016b304d38 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -316,11 +316,15 @@ async def test_form_validate_exception(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" + """Test we handle cannot connect error, then succeed after retry.""" + + # Start the flow result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "edit"} ) + assert result["type"] is FlowResultType.FORM + # First attempt: simulate cannot connect with patch( "pysqueezebox.Server.async_query", return_value=False, @@ -330,17 +334,47 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: { CONF_HOST: HOST, CONF_PORT: PORT, - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONF_USERNAME: "", + CONF_PASSWORD: "", }, ) + # We should still be in a form, with an error assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} + # Second attempt: simulate a successful connection + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == HOST # the flow uses host as title + assert result["data"] == { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + } + assert result["context"]["unique_id"] == UUID + async def test_discovery(hass: HomeAssistant) -> None: - """Test handling of discovered server.""" + """Test handling of discovered server, then completing the flow.""" + + # Initial discovery: server responds with a uuid with patch( "pysqueezebox.Server.async_query", return_value={"uuid": UUID}, @@ -350,24 +384,109 @@ async def test_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "edit" + + # Discovery puts us into the edit step + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + + # Complete the edit step with user input + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + + # Flow should now complete with a config entry + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + } + assert result["context"]["unique_id"] == UUID async def test_discovery_no_uuid(hass: HomeAssistant) -> None: - """Test handling of discovered server with unavailable uuid.""" - with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): + """Test discovery without uuid first fails, then succeeds when uuid is available.""" + + # Initial discovery: no uuid returned + with patch( + "pysqueezebox.Server.async_query", + new=patch_async_query_unauthorized, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, CONF_HTTPS: False}, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "edit" + + # Flow shows the edit form + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + + # First attempt to complete: still no uuid → error on the form + with patch( + "pysqueezebox.Server.async_query", + new=patch_async_query_unauthorized, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + # Second attempt: now the server responds with a uuid + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + + # Flow should now complete successfully + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + } + assert result["context"]["unique_id"] == UUID async def test_dhcp_discovery(hass: HomeAssistant) -> None: - """Test we can process discovery from dhcp.""" + """Test we can process discovery from dhcp and complete the flow.""" + with ( patch( "pysqueezebox.Server.async_query", @@ -382,17 +501,48 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DhcpServiceInfo( - ip="1.1.1.1", + ip=HOST, macaddress="aabbccddeeff", hostname="any", ), ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "edit" + + # DHCP discovery puts us into the edit step + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + + # Complete the edit step with user input + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + + # Flow should now complete with a config entry + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + } + assert result["context"]["unique_id"] == UUID async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: """Test we can handle dhcp discovery when no server is found.""" + with ( patch( "homeassistant.components.squeezebox.config_flow.async_discover", @@ -404,13 +554,43 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DhcpServiceInfo( - ip="1.1.1.1", + ip=HOST, macaddress="aabbccddeeff", hostname="any", ), ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + + # First step: user form with only host + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # Provide just the host to move into edit step + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + + # Now try to complete the edit step with full schema + with patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_failed_discover, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_HTTPS: False, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "edit" + assert result["errors"] == {"base": "unknown"} async def test_dhcp_discovery_existing_player(