From 46c604fcbe8a29b3232bcce4498b509818a41c25 Mon Sep 17 00:00:00 2001 From: Niv Steingarten Date: Tue, 18 Feb 2025 15:23:25 +0200 Subject: [PATCH 1/5] Bump pyrympro from 0.0.8 to 0.0.9 (#138753) --- homeassistant/components/rympro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rympro/manifest.json b/homeassistant/components/rympro/manifest.json index 046e778f05b..51c26b312fb 100644 --- a/homeassistant/components/rympro/manifest.json +++ b/homeassistant/components/rympro/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rympro", "iot_class": "cloud_polling", - "requirements": ["pyrympro==0.0.8"] + "requirements": ["pyrympro==0.0.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0e3bb9ffc9..7f5ddd7a351 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2259,7 +2259,7 @@ pyrituals==0.0.6 pyroute2==0.7.5 # homeassistant.components.rympro -pyrympro==0.0.8 +pyrympro==0.0.9 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fec5fd1673..26f76b8e58b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1843,7 +1843,7 @@ pyrituals==0.0.6 pyroute2==0.7.5 # homeassistant.components.rympro -pyrympro==0.0.8 +pyrympro==0.0.9 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 From 22c634e626d00a8ff40dadbdf64c43b00915271e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 18 Feb 2025 15:16:44 +0100 Subject: [PATCH 2/5] Don't allow setting backup retention to 0 days or copies (#138771) * Don't allow setting backup retention to 0 days or copies * Add tests --- homeassistant/components/backup/store.py | 9 +- homeassistant/components/backup/websocket.py | 6 +- .../backup/snapshots/test_store.ambr | 99 +++++++++- .../backup/snapshots/test_websocket.ambr | 176 ++++++++++++++++-- tests/components/backup/test_store.py | 32 ++++ tests/components/backup/test_websocket.py | 16 +- 6 files changed, 312 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 9b4af823c77..8287080b5a2 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: STORE_DELAY_SAVE = 30 STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 3 +STORAGE_VERSION_MINOR = 4 class StoredBackupData(TypedDict): @@ -60,6 +60,13 @@ class _BackupStore(Store[StoredBackupData]): else: data["config"]["schedule"]["days"] = [state] data["config"]["schedule"]["recurrence"] = "custom_days" + if old_minor_version < 4: + # Workaround for a bug in frontend which incorrectly set days to 0 + # instead of to None for unlimited retention. + if data["config"]["retention"]["copies"] == 0: + data["config"]["retention"]["copies"] = None + if data["config"]["retention"]["days"] == 0: + data["config"]["retention"]["days"] = None # Note: We allow reading data with major version 2. # Reject if major version is higher than 2. diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index b6d092e1913..8453046cabb 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -368,8 +368,10 @@ async def handle_config_info( ), vol.Optional("retention"): vol.Schema( { - vol.Optional("copies"): vol.Any(int, None), - vol.Optional("days"): vol.Any(int, None), + # Note: We can't use cv.positive_int because it allows 0 even + # though 0 is not positive. + vol.Optional("copies"): vol.Any(vol.All(int, vol.Range(min=1)), None), + vol.Optional("days"): vol.Any(vol.All(int, vol.Range(min=1)), None), }, ), vol.Optional("schedule"): vol.Schema( diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index 2fd81d6841a..04f88b84a97 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -39,7 +39,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -84,11 +84,100 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- # name: test_store_migration[store_data1] + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_agent_ids': list([ + 'test.remote', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 4, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data1].1 + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_agent_ids': list([ + 'test.remote', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 4, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data2] dict({ 'data': dict({ 'backups': list([ @@ -131,11 +220,11 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- -# name: test_store_migration[store_data1].1 +# name: test_store_migration[store_data2].1 dict({ 'data': dict({ 'backups': list([ @@ -179,7 +268,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 4452d191d5a..19a85de62ad 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -686,7 +686,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -800,7 +800,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -914,7 +914,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1038,7 +1038,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1205,7 +1205,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1319,7 +1319,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1435,7 +1435,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1549,7 +1549,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1667,7 +1667,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1789,7 +1789,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -1903,7 +1903,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2017,7 +2017,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2131,7 +2131,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2245,7 +2245,7 @@ }), }), 'key': 'backup', - 'minor_version': 3, + 'minor_version': 4, 'version': 1, }) # --- @@ -2323,6 +2323,154 @@ 'type': 'result', }) # --- +# name: test_config_update_errors[command10] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command10].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command11] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_update_errors[command11].1 + dict({ + 'id': 3, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- # name: test_config_update_errors[command1] dict({ 'id': 1, diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index f05afbea9ec..eff53bda777 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -57,6 +57,38 @@ def mock_delay_save() -> Generator[None]: "key": DOMAIN, "version": 1, }, + { + "data": { + "backups": [ + { + "backup_id": "abc123", + "failed_agent_ids": ["test.remote"], + } + ], + "config": { + "create_backup": { + "agent_ids": [], + "include_addons": None, + "include_all_addons": False, + "include_database": True, + "include_folders": None, + "name": None, + "password": None, + }, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "retention": { + "copies": 0, + "days": 0, + }, + "schedule": { + "state": "never", + }, + }, + }, + "key": DOMAIN, + "version": 1, + }, { "data": { "backups": [ diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 8632fb1e957..5e9d7f3c70a 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -1361,6 +1361,14 @@ async def test_config_update( "type": "backup/config/update", "agents": {"test-agent1": {"favorite": True}}, }, + { + "type": "backup/config/update", + "retention": {"copies": 0}, + }, + { + "type": "backup/config/update", + "retention": {"days": 0}, + }, ], ) async def test_config_update_errors( @@ -2158,7 +2166,7 @@ async def test_config_schedule_logic( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "retention": {"copies": 0, "days": None}, + "retention": {"copies": 1, "days": None}, "schedule": {"recurrence": "daily"}, }, { @@ -2232,7 +2240,7 @@ async def test_config_schedule_logic( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "retention": {"copies": 0, "days": None}, + "retention": {"copies": 1, "days": None}, "schedule": {"recurrence": "daily"}, }, { @@ -2301,7 +2309,7 @@ async def test_config_schedule_logic( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test.test-agent"]}, - "retention": {"copies": 0, "days": None}, + "retention": {"copies": 1, "days": None}, "schedule": {"recurrence": "daily"}, }, { @@ -3019,7 +3027,7 @@ async def test_config_retention_copies_logic_manual_backup( { "type": "backup/config/update", "create_backup": {"agent_ids": ["test-agent"]}, - "retention": {"copies": None, "days": 0}, + "retention": {"copies": None, "days": 1}, "schedule": {"recurrence": "never"}, } ], From a003f89a5ea640f59d8b148934ccfb763d2562f1 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 18 Feb 2025 16:17:13 +0200 Subject: [PATCH 3/5] Fix Z-WaveJS inclusion in the background (#138717) * Fix Z-WaveJS inclusion in the background * improve async handling * just return the `requested_grant` to the driver * handle controller busy state --- homeassistant/components/zwave_js/api.py | 22 ++++++++++++++++++---- tests/components/zwave_js/test_api.py | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 37ce9a51c91..aef23cb73ea 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -805,7 +805,7 @@ async def websocket_add_node( ] msg[DATA_UNSUBSCRIBE] = unsubs - if controller.inclusion_state == InclusionState.INCLUDING: + if controller.inclusion_state in (InclusionState.INCLUDING, InclusionState.BUSY): connection.send_result( msg[ID], True, # Inclusion is already in progress @@ -883,6 +883,11 @@ async def websocket_subscribe_s2_inclusion( ) -> None: """Subscribe to S2 inclusion initiated by the controller.""" + @callback + def async_cleanup() -> None: + for unsub in unsubs: + unsub() + @callback def forward_dsk(event: dict) -> None: connection.send_message( @@ -891,9 +896,18 @@ async def websocket_subscribe_s2_inclusion( ) ) - unsub = driver.controller.on("validate dsk and enter pin", forward_dsk) - connection.subscriptions[msg["id"]] = unsub - msg[DATA_UNSUBSCRIBE] = [unsub] + @callback + def handle_requested_grant(event: dict) -> None: + """Accept the requested security classes without user interaction.""" + hass.async_create_task( + driver.controller.async_grant_security_classes(event["requested_grant"]) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + msg[DATA_UNSUBSCRIBE] = unsubs = [ + driver.controller.on("grant security classes", handle_requested_grant), + driver.controller.on("validate dsk and enter pin", forward_dsk), + ] connection.send_result(msg[ID]) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 6f341f8f77b..42c5d59d7ad 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5284,6 +5284,20 @@ async def test_subscribe_s2_inclusion( assert msg["success"] assert msg["result"] is None + # Test receiving requested grant event + event = Event( + type="grant security classes", + data={ + "source": "controller", + "event": "grant security classes", + "requested": { + "securityClasses": [SecurityClass.S2_UNAUTHENTICATED], + "clientSideAuth": False, + }, + }, + ) + client.driver.receive_event(event) + # Test receiving DSK request event event = Event( type="validate dsk and enter pin", From e9fcef1b570bc80e6ab25cc7e17b70b9cbcfc02d Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:43:00 -0500 Subject: [PATCH 4/5] Fix TV input source option for Sonos Arc Ultra (#138778) initial commit --- homeassistant/components/sonos/const.py | 1 + tests/components/sonos/conftest.py | 10 +++++++-- tests/components/sonos/test_media_player.py | 25 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 610a68afedf..8fb704cbfbc 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -170,6 +170,7 @@ MODELS_TV_ONLY = ( "BEAM", "PLAYBAR", "PLAYBASE", + "ULTRA", ) MODELS_LINEIN_AND_TV = ("AMP",) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 0f56794b9f2..e22f18c6d77 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -580,13 +580,19 @@ def alarm_clock_fixture_extended(): return alarm_clock +@pytest.fixture(name="speaker_model") +def speaker_model_fixture(request: pytest.FixtureRequest): + """Create fixture for the speaker model.""" + return getattr(request, "param", "Model Name") + + @pytest.fixture(name="speaker_info") -def speaker_info_fixture(): +def speaker_info_fixture(speaker_model): """Create speaker_info fixture.""" return { "zone_name": "Zone A", "uid": "RINCON_test", - "model_name": "Model Name", + "model_name": speaker_model, "model_number": "S12", "hardware_version": "1.20.1.6-1.1", "software_version": "49.2-64250", diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 63b2c8889ec..cec40c997a7 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -10,6 +10,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, @@ -1205,3 +1206,27 @@ async def test_media_get_queue( ) soco_mock.get_queue.assert_called_with(max_items=0) assert result == snapshot + + +@pytest.mark.parametrize( + ("speaker_model", "source_list"), + [ + ("Sonos Arc Ultra", [SOURCE_TV]), + ("Sonos Arc", [SOURCE_TV]), + ("Sonos Playbar", [SOURCE_TV]), + ("Sonos Connect", [SOURCE_LINEIN]), + ("Sonos Play:5", [SOURCE_LINEIN]), + ("Sonos Amp", [SOURCE_LINEIN, SOURCE_TV]), + ("Sonos Era", None), + ], + indirect=["speaker_model"], +) +async def test_media_source_list( + hass: HomeAssistant, + async_autosetup_sonos, + speaker_model: str, + source_list: list[str] | None, +) -> None: + """Test the mapping between the speaker model name and source_list.""" + state = hass.states.get("media_player.zone_a") + assert state.attributes.get(ATTR_INPUT_SOURCE_LIST) == source_list From a45fb57595f0f545880026f9205da3087f12ae8a Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 18 Feb 2025 15:43:51 +0100 Subject: [PATCH 5/5] Fix grammar in evohome.reset_system action, consistently add "mode" (#138777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix grammar in evohome.reset_system action, consistently add "mode" - fix the grammar with "Sets … and resets …" - add "mode" to all mode names for consistency * Revert, removing one excessive "mode" --- homeassistant/components/evohome/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/evohome/strings.json b/homeassistant/components/evohome/strings.json index ca032643c9d..4fc51c30b97 100644 --- a/homeassistant/components/evohome/strings.json +++ b/homeassistant/components/evohome/strings.json @@ -10,17 +10,17 @@ }, "period": { "name": "Period", - "description": "A period of time in days; used only with Away, DayOff, or Custom. The system will revert to Auto at midnight (up to 99 days, today is day 1)." + "description": "A period of time in days; used only with Away, DayOff, or Custom mode. The system will revert to Auto mode at midnight (up to 99 days, today is day 1)." }, "duration": { "name": "Duration", - "description": "The duration in hours; used only with AutoWithEco (up to 24 hours)." + "description": "The duration in hours; used only with AutoWithEco mode (up to 24 hours)." } } }, "reset_system": { "name": "Reset system", - "description": "Sets the system to Auto mode and reset all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. AutoWithReset mode)." + "description": "Sets the system to Auto mode and resets all the zones to follow their schedules. Not all Evohome systems support this feature (i.e. AutoWithReset mode)." }, "refresh_system": { "name": "Refresh system",