From 9adf493acdd0d1caf041836e04bee21599bfced6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 18 Jun 2025 17:58:50 +0100 Subject: [PATCH 01/69] Use non-autospec mock for Reolink's init tests (#146991) --- tests/components/reolink/conftest.py | 28 ++++ tests/components/reolink/test_host.py | 1 + tests/components/reolink/test_init.py | 224 +++++++++++--------------- 3 files changed, 125 insertions(+), 128 deletions(-) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index d96931aaf26..69eeee99fab 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -65,11 +65,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: def _init_host_mock(host_mock: MagicMock) -> None: host_mock.get_host_data = AsyncMock(return_value=None) host_mock.get_states = AsyncMock(return_value=None) + host_mock.get_state = AsyncMock() host_mock.check_new_firmware = AsyncMock(return_value=False) + host_mock.subscribe = AsyncMock() host_mock.unsubscribe = AsyncMock(return_value=True) host_mock.logout = AsyncMock(return_value=True) host_mock.reboot = AsyncMock() host_mock.set_ptz_command = AsyncMock() + host_mock.get_motion_state_all_ch = AsyncMock(return_value=False) host_mock.is_nvr = True host_mock.is_hub = False host_mock.mac_address = TEST_MAC @@ -138,8 +141,10 @@ def _init_host_mock(host_mock: MagicMock) -> None: # Disable tcp push by default for tests host_mock.baichuan.port = TEST_BC_PORT host_mock.baichuan.events_active = False + host_mock.baichuan.subscribe_events = AsyncMock() host_mock.baichuan.unsubscribe_events = AsyncMock() host_mock.baichuan.check_subscribe_events = AsyncMock() + host_mock.baichuan.get_privacy_mode = AsyncMock() host_mock.baichuan.mac_address.return_value = TEST_MAC_CAM host_mock.baichuan.privacy_mode.return_value = False host_mock.baichuan.day_night_state.return_value = "day" @@ -242,3 +247,26 @@ def test_chime(reolink_connect: MagicMock) -> None: reolink_connect.chime_list = [TEST_CHIME] reolink_connect.chime.return_value = TEST_CHIME return TEST_CHIME + + +@pytest.fixture +def reolink_chime(reolink_host: MagicMock) -> None: + """Mock a reolink chime.""" + TEST_CHIME = Chime( + host=reolink_host, + dev_id=12345678, + channel=0, + ) + TEST_CHIME.name = "Test chime" + TEST_CHIME.volume = 3 + TEST_CHIME.connect_state = 2 + TEST_CHIME.led_state = True + TEST_CHIME.event_info = { + "md": {"switch": 0, "musicId": 0}, + "people": {"switch": 0, "musicId": 1}, + "visitor": {"switch": 1, "musicId": 2}, + } + + reolink_host.chime_list = [TEST_CHIME] + reolink_host.chime.return_value = TEST_CHIME + return TEST_CHIME diff --git a/tests/components/reolink/test_host.py b/tests/components/reolink/test_host.py index 3c2f434ccc7..f997a1ac08a 100644 --- a/tests/components/reolink/test_host.py +++ b/tests/components/reolink/test_host.py @@ -115,6 +115,7 @@ async def test_webhook_callback( assert hass.states.get(entity_id).state == STATE_OFF # test webhook callback success all channels + reolink_connect.get_motion_state_all_ch.return_value = True reolink_connect.motion_detected.return_value = True reolink_connect.ONVIF_event_callback.return_value = None await client.post(f"/api/webhook/{webhook_id}") diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 482928560b9..ed71314e961 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -69,7 +69,7 @@ from .conftest import ( from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import WebSocketGenerator -pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") +pytestmark = pytest.mark.usefixtures("reolink_host", "reolink_platforms") CHIME_MODEL = "Reolink Chime" @@ -116,15 +116,14 @@ async def test_wait(*args, **key_args) -> None: ) async def test_failures_parametrized( hass: HomeAssistant, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, attr: str, value: Any, expected: ConfigEntryState, ) -> None: """Test outcomes when changing errors.""" - original = getattr(reolink_connect, attr) - setattr(reolink_connect, attr, value) + setattr(reolink_host, attr, value) assert await hass.config_entries.async_setup(config_entry.entry_id) is ( expected is ConfigEntryState.LOADED ) @@ -132,17 +131,15 @@ async def test_failures_parametrized( assert config_entry.state == expected - setattr(reolink_connect, attr, original) - async def test_firmware_error_twice( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test when the firmware update fails 2 times.""" - reolink_connect.check_new_firmware.side_effect = ReolinkError("Test error") + reolink_host.check_new_firmware.side_effect = ReolinkError("Test error") with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -158,13 +155,11 @@ async def test_firmware_error_twice( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - reolink_connect.check_new_firmware.reset_mock(side_effect=True) - async def test_credential_error_three( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry, ) -> None: @@ -174,7 +169,7 @@ async def test_credential_error_three( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED - reolink_connect.get_states.side_effect = CredentialsInvalidError("Test error") + reolink_host.get_states.side_effect = CredentialsInvalidError("Test error") issue_id = f"config_entry_reauth_{DOMAIN}_{config_entry.entry_id}" for _ in range(NUM_CRED_ERRORS): @@ -185,31 +180,26 @@ async def test_credential_error_three( assert (HOMEASSISTANT_DOMAIN, issue_id) in issue_registry.issues - reolink_connect.get_states.reset_mock(side_effect=True) - async def test_entry_reloading( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test the entry is reloaded correctly when settings change.""" - reolink_connect.is_nvr = False - reolink_connect.logout.reset_mock() + reolink_host.is_nvr = False assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert reolink_connect.logout.call_count == 0 + assert reolink_host.logout.call_count == 0 assert config_entry.title == "test_reolink_name" hass.config_entries.async_update_entry(config_entry, title="New Name") await hass.async_block_till_done() - assert reolink_connect.logout.call_count == 1 + assert reolink_host.logout.call_count == 1 assert config_entry.title == "New Name" - reolink_connect.is_nvr = True - @pytest.mark.parametrize( ("attr", "value", "expected_models"), @@ -241,7 +231,7 @@ async def test_removing_disconnected_cams( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, attr: str | None, @@ -249,7 +239,7 @@ async def test_removing_disconnected_cams( expected_models: list[str], ) -> None: """Test device and entity registry are cleaned up when camera is removed.""" - reolink_connect.channels = [0] + reolink_host.channels = [0] assert await async_setup_component(hass, "config", {}) client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device @@ -265,8 +255,7 @@ async def test_removing_disconnected_cams( # Try to remove the device after 'disconnecting' a camera. if attr is not None: - original = getattr(reolink_connect, attr) - setattr(reolink_connect, attr, value) + setattr(reolink_host, attr, value) expected_success = TEST_CAM_MODEL not in expected_models for device in device_entries: if device.model == TEST_CAM_MODEL: @@ -279,9 +268,6 @@ async def test_removing_disconnected_cams( device_models = [device.model for device in device_entries] assert sorted(device_models) == sorted(expected_models) - if attr is not None: - setattr(reolink_connect, attr, original) - @pytest.mark.parametrize( ("attr", "value", "expected_models"), @@ -307,8 +293,8 @@ async def test_removing_chime( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, - reolink_connect: MagicMock, - test_chime: Chime, + reolink_host: MagicMock, + reolink_chime: Chime, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, attr: str | None, @@ -316,7 +302,7 @@ async def test_removing_chime( expected_models: list[str], ) -> None: """Test removing a chime.""" - reolink_connect.channels = [0] + reolink_host.channels = [0] assert await async_setup_component(hass, "config", {}) client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device @@ -336,11 +322,11 @@ async def test_removing_chime( async def test_remove_chime(*args, **key_args): """Remove chime.""" - test_chime.connect_state = -1 + reolink_chime.connect_state = -1 - test_chime.remove = test_remove_chime + reolink_chime.remove = test_remove_chime elif attr is not None: - setattr(test_chime, attr, value) + setattr(reolink_chime, attr, value) # Try to remove the device after 'disconnecting' a chime. expected_success = CHIME_MODEL not in expected_models @@ -444,7 +430,7 @@ async def test_removing_chime( async def test_migrate_entity_ids( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, original_id: str, @@ -464,8 +450,8 @@ async def test_migrate_entity_ids( return support_ch_uid return True - reolink_connect.channels = [0] - reolink_connect.supported = mock_supported + reolink_host.channels = [0] + reolink_host.supported = mock_supported dev_entry = device_registry.async_get_or_create( identifiers={(DOMAIN, original_dev_id)}, @@ -513,7 +499,7 @@ async def test_migrate_entity_ids( async def test_migrate_with_already_existing_device( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: @@ -529,8 +515,8 @@ async def test_migrate_with_already_existing_device( return True return True - reolink_connect.channels = [0] - reolink_connect.supported = mock_supported + reolink_host.channels = [0] + reolink_host.supported = mock_supported device_registry.async_get_or_create( identifiers={(DOMAIN, new_dev_id)}, @@ -562,7 +548,7 @@ async def test_migrate_with_already_existing_device( async def test_migrate_with_already_existing_entity( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: @@ -579,8 +565,8 @@ async def test_migrate_with_already_existing_entity( return True return True - reolink_connect.channels = [0] - reolink_connect.supported = mock_supported + reolink_host.channels = [0] + reolink_host.supported = mock_supported dev_entry = device_registry.async_get_or_create( identifiers={(DOMAIN, dev_id)}, @@ -623,13 +609,13 @@ async def test_migrate_with_already_existing_entity( async def test_cleanup_mac_connection( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test cleanup of the MAC of a IPC which was set to the MAC of the host.""" - reolink_connect.channels = [0] - reolink_connect.baichuan.mac_address.return_value = None + reolink_host.channels = [0] + reolink_host.baichuan.mac_address.return_value = None entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" dev_id = f"{TEST_UID}_{TEST_UID_CAM}" domain = Platform.SWITCH @@ -666,19 +652,17 @@ async def test_cleanup_mac_connection( assert device assert device.connections == set() - reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM - async def test_cleanup_combined_with_NVR( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test cleanup of the device registry if IPC camera device was combined with the NVR device.""" - reolink_connect.channels = [0] - reolink_connect.baichuan.mac_address.return_value = None + reolink_host.channels = [0] + reolink_host.baichuan.mac_address.return_value = None entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" dev_id = f"{TEST_UID}_{TEST_UID_CAM}" domain = Platform.SWITCH @@ -726,18 +710,16 @@ async def test_cleanup_combined_with_NVR( ("OTHER_INTEGRATION", "SOME_ID"), } - reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM - async def test_cleanup_hub_and_direct_connection( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, ) -> None: """Test cleanup of the device registry if IPC camera device was connected directly and through the hub/NVR.""" - reolink_connect.channels = [0] + reolink_host.channels = [0] entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" dev_id = f"{TEST_UID}_{TEST_UID_CAM}" domain = Platform.SWITCH @@ -801,11 +783,11 @@ async def test_no_repair_issue( async def test_https_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when https local url is used.""" - reolink_connect.get_states = test_wait + reolink_host.get_states = test_wait await async_process_ha_core_config( hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"} ) @@ -828,11 +810,11 @@ async def test_https_repair_issue( async def test_ssl_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when global ssl certificate is used.""" - reolink_connect.get_states = test_wait + reolink_host.get_states = test_wait assert await async_setup_component(hass, "webhook", {}) hass.config.api.use_ssl = True @@ -859,32 +841,30 @@ async def test_ssl_repair_issue( async def test_port_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, protocol: str, issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when auto enable of ports fails.""" - reolink_connect.set_net_port.side_effect = ReolinkError("Test error") - reolink_connect.onvif_enabled = False - reolink_connect.rtsp_enabled = False - reolink_connect.rtmp_enabled = False - reolink_connect.protocol = protocol + reolink_host.set_net_port.side_effect = ReolinkError("Test error") + reolink_host.onvif_enabled = False + reolink_host.rtsp_enabled = False + reolink_host.rtmp_enabled = False + reolink_host.protocol = protocol assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert (DOMAIN, "enable_port") in issue_registry.issues - reolink_connect.set_net_port.reset_mock(side_effect=True) - async def test_webhook_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: """Test repairs issue is raised when the webhook url is unreachable.""" - reolink_connect.get_states = test_wait + reolink_host.get_states = test_wait with ( patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0), patch( @@ -903,25 +883,24 @@ async def test_webhook_repair_issue( async def test_firmware_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: """Test firmware issue is raised when too old firmware is used.""" - reolink_connect.camera_sw_version_update_required.return_value = True + reolink_host.camera_sw_version_update_required.return_value = True assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert (DOMAIN, "firmware_update_host") in issue_registry.issues - reolink_connect.camera_sw_version_update_required.return_value = False async def test_password_too_long_repair_issue( hass: HomeAssistant, - reolink_connect: MagicMock, + reolink_host: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: """Test password too long issue is raised.""" - reolink_connect.valid_password.return_value = False + reolink_host.valid_password.return_value = False config_entry = MockConfigEntry( domain=DOMAIN, unique_id=format_mac(TEST_MAC), @@ -946,13 +925,12 @@ async def test_password_too_long_repair_issue( DOMAIN, f"password_too_long_{config_entry.entry_id}", ) in issue_registry.issues - reolink_connect.valid_password.return_value = True async def test_new_device_discovered( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test the entry is reloaded when a new camera or chime is detected.""" @@ -960,26 +938,24 @@ async def test_new_device_discovered( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - reolink_connect.logout.reset_mock() - - assert reolink_connect.logout.call_count == 0 - reolink_connect.new_devices = True + assert reolink_host.logout.call_count == 0 + reolink_host.new_devices = True freezer.tick(DEVICE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - assert reolink_connect.logout.call_count == 1 + assert reolink_host.logout.call_count == 1 async def test_port_changed( hass: HomeAssistant, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test config_entry port update when it has changed during initial login.""" assert config_entry.data[CONF_PORT] == TEST_PORT - reolink_connect.port = 4567 + reolink_host.port = 4567 assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -989,12 +965,12 @@ async def test_port_changed( async def test_baichuan_port_changed( hass: HomeAssistant, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test config_entry baichuan port update when it has changed during initial login.""" assert config_entry.data[CONF_BC_PORT] == TEST_BC_PORT - reolink_connect.baichuan.port = 8901 + reolink_host.baichuan.port = 8901 assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1005,14 +981,12 @@ async def test_baichuan_port_changed( async def test_privacy_mode_on( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test successful setup even when privacy mode is turned on.""" - reolink_connect.baichuan.privacy_mode.return_value = True - reolink_connect.get_states = AsyncMock( - side_effect=LoginPrivacyModeError("Test error") - ) + reolink_host.baichuan.privacy_mode.return_value = True + reolink_host.get_states = AsyncMock(side_effect=LoginPrivacyModeError("Test error")) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -1020,40 +994,36 @@ async def test_privacy_mode_on( assert config_entry.state == ConfigEntryState.LOADED - reolink_connect.baichuan.privacy_mode.return_value = False - async def test_LoginPrivacyModeError( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test normal update when get_states returns a LoginPrivacyModeError.""" - reolink_connect.baichuan.privacy_mode.return_value = False - reolink_connect.get_states = AsyncMock( - side_effect=LoginPrivacyModeError("Test error") - ) + reolink_host.baichuan.privacy_mode.return_value = False + reolink_host.get_states = AsyncMock(side_effect=LoginPrivacyModeError("Test error")) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - reolink_connect.baichuan.check_subscribe_events.reset_mock() - assert reolink_connect.baichuan.check_subscribe_events.call_count == 0 + reolink_host.baichuan.check_subscribe_events.reset_mock() + assert reolink_host.baichuan.check_subscribe_events.call_count == 0 freezer.tick(DEVICE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() - assert reolink_connect.baichuan.check_subscribe_events.call_count >= 1 + assert reolink_host.baichuan.check_subscribe_events.call_count >= 1 async def test_privacy_mode_change_callback( hass: HomeAssistant, freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test privacy mode changed callback.""" @@ -1068,13 +1038,12 @@ async def test_privacy_mode_change_callback( callback_mock = callback_mock_class() - reolink_connect.model = TEST_HOST_MODEL - reolink_connect.baichuan.events_active = True - reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True) - reolink_connect.baichuan.register_callback = callback_mock.register_callback - reolink_connect.baichuan.privacy_mode.return_value = True - reolink_connect.audio_record.return_value = True - reolink_connect.get_states = AsyncMock() + reolink_host.model = TEST_HOST_MODEL + reolink_host.baichuan.events_active = True + reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True) + reolink_host.baichuan.register_callback = callback_mock.register_callback + reolink_host.baichuan.privacy_mode.return_value = True + reolink_host.audio_record.return_value = True with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -1085,29 +1054,29 @@ async def test_privacy_mode_change_callback( assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # simulate a TCP push callback signaling a privacy mode change - reolink_connect.baichuan.privacy_mode.return_value = False + reolink_host.baichuan.privacy_mode.return_value = False assert callback_mock.callback_func is not None callback_mock.callback_func() # check that a coordinator update was scheduled. - reolink_connect.get_states.reset_mock() - assert reolink_connect.get_states.call_count == 0 + reolink_host.get_states.reset_mock() + assert reolink_host.get_states.call_count == 0 freezer.tick(5) async_fire_time_changed(hass) await hass.async_block_till_done() - assert reolink_connect.get_states.call_count >= 1 + assert reolink_host.get_states.call_count >= 1 assert hass.states.get(entity_id).state == STATE_ON # test cleanup during unloading, first reset to privacy mode ON - reolink_connect.baichuan.privacy_mode.return_value = True + reolink_host.baichuan.privacy_mode.return_value = True callback_mock.callback_func() freezer.tick(5) async_fire_time_changed(hass) await hass.async_block_till_done() # now fire the callback again, but unload before refresh took place - reolink_connect.baichuan.privacy_mode.return_value = False + reolink_host.baichuan.privacy_mode.return_value = False callback_mock.callback_func() await hass.async_block_till_done() @@ -1120,7 +1089,7 @@ async def test_camera_wake_callback( hass: HomeAssistant, freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test camera wake callback.""" @@ -1135,13 +1104,12 @@ async def test_camera_wake_callback( callback_mock = callback_mock_class() - reolink_connect.model = TEST_HOST_MODEL - reolink_connect.baichuan.events_active = True - reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True) - reolink_connect.baichuan.register_callback = callback_mock.register_callback - reolink_connect.sleeping.return_value = True - reolink_connect.audio_record.return_value = True - reolink_connect.get_states = AsyncMock() + reolink_host.model = TEST_HOST_MODEL + reolink_host.baichuan.events_active = True + reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True) + reolink_host.baichuan.register_callback = callback_mock.register_callback + reolink_host.sleeping.return_value = True + reolink_host.audio_record.return_value = True with ( patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]), @@ -1157,12 +1125,12 @@ async def test_camera_wake_callback( entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio" assert hass.states.get(entity_id).state == STATE_ON - reolink_connect.sleeping.return_value = False - reolink_connect.get_states.reset_mock() - assert reolink_connect.get_states.call_count == 0 + reolink_host.sleeping.return_value = False + reolink_host.get_states.reset_mock() + assert reolink_host.get_states.call_count == 0 # simulate a TCP push callback signaling the battery camera woke up - reolink_connect.audio_record.return_value = False + reolink_host.audio_record.return_value = False assert callback_mock.callback_func is not None with ( patch( @@ -1182,7 +1150,7 @@ async def test_camera_wake_callback( await hass.async_block_till_done() # check that a coordinator update was scheduled. - assert reolink_connect.get_states.call_count >= 1 + assert reolink_host.get_states.call_count >= 1 assert hass.states.get(entity_id).state == STATE_OFF @@ -1201,7 +1169,7 @@ async def test_baichaun_only( async def test_remove( hass: HomeAssistant, - reolink_connect: MagicMock, + reolink_host: MagicMock, config_entry: MockConfigEntry, ) -> None: """Test removing of the reolink integration.""" From 6befd065a161119e3423bd0cebe273665c7b7e0d Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 18 Jun 2025 15:49:44 -0500 Subject: [PATCH 02/69] Bump aioesphomeapi to 32.2.4 (#147100) Bump aioesphomeapi --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 9b70aba4de1..6142b9ce5ec 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==32.2.1", + "aioesphomeapi==32.2.4", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index b69e1e69ccd..28ee85d6565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.1 +aioesphomeapi==32.2.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8254ce9f261..dcc354338e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.1 +aioesphomeapi==32.2.4 # homeassistant.components.flo aioflo==2021.11.0 From 8d8ff011fcc8c7fd3cebcaaa1f96f52e010c7095 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Jun 2025 01:17:12 +0200 Subject: [PATCH 03/69] Minor improvements of service helper (#147079) --- homeassistant/helpers/service.py | 9 ++++++--- tests/helpers/test_service.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 6e1988fe4cd..4a10dfc5616 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -718,7 +718,6 @@ async def async_get_all_descriptions( for service_name in services_by_domain } # If we have a complete cache, check if it is still valid - all_cache: tuple[set[tuple[str, str]], dict[str, dict[str, Any]]] | None if all_cache := hass.data.get(ALL_SERVICE_DESCRIPTIONS_CACHE): previous_all_services, previous_descriptions_cache = all_cache # If the services are the same, we can return the cache @@ -744,7 +743,11 @@ async def async_get_all_descriptions( continue if TYPE_CHECKING: assert isinstance(int_or_exc, Exception) - _LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc) + _LOGGER.error( + "Failed to load services.yaml for integration: %s", + domain, + exc_info=int_or_exc, + ) if integrations: loaded = await hass.async_add_executor_job( @@ -772,7 +775,7 @@ async def async_get_all_descriptions( # Cache missing descriptions domain_yaml = loaded.get(domain) or {} # The YAML may be empty for dynamically defined - # services (ie shell_command) that never call + # services (e.g. shell_command) that never call # service.async_set_service_schema for the dynamic # service diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 6b464faa110..5d018f5f3ee 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1141,7 +1141,7 @@ async def test_async_get_all_descriptions_failing_integration( descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 3 - assert "Failed to load integration: logger" in caplog.text + assert "Failed to load services.yaml for integration: logger" in caplog.text # Services are empty defaults if the load fails but should # not raise From 3dba7e5bd24035991358507b921204f06ac0c7de Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 18 Jun 2025 21:12:37 -0500 Subject: [PATCH 04/69] Send intent progress events to ESPHome (#146966) --- homeassistant/components/esphome/assist_satellite.py | 7 +++++++ tests/components/esphome/test_assist_satellite.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/esphome/assist_satellite.py b/homeassistant/components/esphome/assist_satellite.py index 53d5d449be8..fdeadd7feb1 100644 --- a/homeassistant/components/esphome/assist_satellite.py +++ b/homeassistant/components/esphome/assist_satellite.py @@ -60,6 +60,7 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[ VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: PipelineEventType.STT_START, VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: PipelineEventType.STT_END, VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: PipelineEventType.INTENT_START, + VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: PipelineEventType.INTENT_PROGRESS, VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END, VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START, VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END, @@ -282,6 +283,12 @@ class EsphomeAssistSatellite( elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: assert event.data is not None data_to_send = {"text": event.data["stt_output"]["text"]} + elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: + data_to_send = { + "tts_start_streaming": bool( + event.data and event.data.get("tts_start_streaming") + ), + } elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: assert event.data is not None data_to_send = { diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index ec6091307b9..71977f0285c 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -240,6 +240,17 @@ async def test_pipeline_api_audio( ) assert satellite.state == AssistSatelliteState.PROCESSING + event_callback( + PipelineEvent( + type=PipelineEventType.INTENT_PROGRESS, + data={"tts_start_streaming": True}, + ) + ) + assert mock_client.send_voice_assistant_event.call_args_list[-1].args == ( + VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS, + {"tts_start_streaming": True}, + ) + event_callback( PipelineEvent( type=PipelineEventType.INTENT_END, From f90a7404290c83fd25115d315140f86008ec6342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 19 Jun 2025 07:03:48 +0100 Subject: [PATCH 05/69] Use non-autospec mock for Reolink's binary_sensor, camera and diag tests (#147095) --- tests/components/reolink/conftest.py | 5 ++- .../components/reolink/test_binary_sensor.py | 34 +++++++++---------- tests/components/reolink/test_camera.py | 14 ++++---- tests/components/reolink/test_diagnostics.py | 4 +-- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 69eeee99fab..0ca5612f8fd 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -73,6 +73,10 @@ def _init_host_mock(host_mock: MagicMock) -> None: host_mock.reboot = AsyncMock() host_mock.set_ptz_command = AsyncMock() host_mock.get_motion_state_all_ch = AsyncMock(return_value=False) + host_mock.get_stream_source = AsyncMock() + host_mock.get_snapshot = AsyncMock() + host_mock.get_encoding = AsyncMock(return_value="h264") + host_mock.ONVIF_event_callback = AsyncMock() host_mock.is_nvr = True host_mock.is_hub = False host_mock.mac_address = TEST_MAC @@ -105,7 +109,6 @@ def _init_host_mock(host_mock: MagicMock) -> None: host_mock.camera_uid.return_value = TEST_UID_CAM host_mock.camera_online.return_value = True host_mock.channel_for_uid.return_value = 0 - host_mock.get_encoding.return_value = "h264" host_mock.firmware_update_available.return_value = False host_mock.session_active = True host_mock.timeout = 60 diff --git a/tests/components/reolink/test_binary_sensor.py b/tests/components/reolink/test_binary_sensor.py index 99c9efba002..e6275a2108e 100644 --- a/tests/components/reolink/test_binary_sensor.py +++ b/tests/components/reolink/test_binary_sensor.py @@ -21,11 +21,11 @@ async def test_motion_sensor( hass_client_no_auth: ClientSessionGenerator, freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test binary sensor entity with motion sensor.""" - reolink_connect.model = TEST_DUO_MODEL - reolink_connect.motion_detected.return_value = True + reolink_host.model = TEST_DUO_MODEL + reolink_host.motion_detected.return_value = True with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -34,7 +34,7 @@ async def test_motion_sensor( entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion_lens_0" assert hass.states.get(entity_id).state == STATE_ON - reolink_connect.motion_detected.return_value = False + reolink_host.motion_detected.return_value = False freezer.tick(DEVICE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -42,8 +42,8 @@ async def test_motion_sensor( assert hass.states.get(entity_id).state == STATE_OFF # test ONVIF webhook callback - reolink_connect.motion_detected.return_value = True - reolink_connect.ONVIF_event_callback.return_value = [0] + reolink_host.motion_detected.return_value = True + reolink_host.ONVIF_event_callback.return_value = [0] webhook_id = config_entry.runtime_data.host.webhook_id client = await hass_client_no_auth() await client.post(f"/api/webhook/{webhook_id}", data="test_data") @@ -56,11 +56,11 @@ async def test_smart_ai_sensor( hass_client_no_auth: ClientSessionGenerator, freezer: FrozenDateTimeFactory, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test smart ai binary sensor entity.""" - reolink_connect.model = TEST_HOST_MODEL - reolink_connect.baichuan.smart_ai_state.return_value = True + reolink_host.model = TEST_HOST_MODEL + reolink_host.baichuan.smart_ai_state.return_value = True with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -69,7 +69,7 @@ async def test_smart_ai_sensor( entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_crossline_zone1_person" assert hass.states.get(entity_id).state == STATE_ON - reolink_connect.baichuan.smart_ai_state.return_value = False + reolink_host.baichuan.smart_ai_state.return_value = False freezer.tick(DEVICE_UPDATE_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -80,7 +80,7 @@ async def test_smart_ai_sensor( async def test_tcp_callback( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test tcp callback using motion sensor.""" @@ -95,11 +95,11 @@ async def test_tcp_callback( callback_mock = callback_mock_class() - reolink_connect.model = TEST_HOST_MODEL - reolink_connect.baichuan.events_active = True - reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True) - reolink_connect.baichuan.register_callback = callback_mock.register_callback - reolink_connect.motion_detected.return_value = True + reolink_host.model = TEST_HOST_MODEL + reolink_host.baichuan.events_active = True + reolink_host.baichuan.subscribe_events.reset_mock(side_effect=True) + reolink_host.baichuan.register_callback = callback_mock.register_callback + reolink_host.motion_detected.return_value = True with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -110,7 +110,7 @@ async def test_tcp_callback( assert hass.states.get(entity_id).state == STATE_ON # simulate a TCP push callback - reolink_connect.motion_detected.return_value = False + reolink_host.motion_detected.return_value = False assert callback_mock.callback_func is not None callback_mock.callback_func() diff --git a/tests/components/reolink/test_camera.py b/tests/components/reolink/test_camera.py index 4f18f769e02..4ab43de225f 100644 --- a/tests/components/reolink/test_camera.py +++ b/tests/components/reolink/test_camera.py @@ -25,7 +25,7 @@ async def test_camera( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test camera entity with fluent.""" with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): @@ -37,28 +37,26 @@ async def test_camera( assert hass.states.get(entity_id).state == CameraState.IDLE # check getting a image from the camera - reolink_connect.get_snapshot.return_value = b"image" + reolink_host.get_snapshot.return_value = b"image" assert (await async_get_image(hass, entity_id)).content == b"image" - reolink_connect.get_snapshot.side_effect = ReolinkError("Test error") + reolink_host.get_snapshot.side_effect = ReolinkError("Test error") with pytest.raises(HomeAssistantError): await async_get_image(hass, entity_id) # check getting the stream source assert await async_get_stream_source(hass, entity_id) is not None - reolink_connect.get_snapshot.reset_mock(side_effect=True) - @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_camera_no_stream_source( hass: HomeAssistant, config_entry: MockConfigEntry, - reolink_connect: MagicMock, + reolink_host: MagicMock, ) -> None: """Test camera entity with no stream source.""" - reolink_connect.model = TEST_DUO_MODEL - reolink_connect.get_stream_source.return_value = None + reolink_host.model = TEST_DUO_MODEL + reolink_host.get_stream_source.return_value = None with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/reolink/test_diagnostics.py b/tests/components/reolink/test_diagnostics.py index d45163d3cf0..b347bae9ec0 100644 --- a/tests/components/reolink/test_diagnostics.py +++ b/tests/components/reolink/test_diagnostics.py @@ -15,8 +15,8 @@ from tests.typing import ClientSessionGenerator async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, - reolink_connect: MagicMock, - test_chime: Chime, + reolink_host: MagicMock, + reolink_chime: Chime, config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: From 6a16424bb47588d81f69c3bc72310e4b53df2f66 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 19 Jun 2025 08:20:19 +0200 Subject: [PATCH 06/69] Fix nightly build (#147110) Update builder.yml --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index dc97e627ea4..d7bbfc8fa5e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -108,7 +108,7 @@ jobs: uses: dawidd6/action-download-artifact@v11 with: github_token: ${{secrets.GITHUB_TOKEN}} - repo: home-assistant/intents-package + repo: OHF-Voice/intents-package branch: main workflow: nightly.yaml workflow_conclusion: success From fada81e1ce47aa42848dddc61921e0e529059f7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 Jun 2025 08:46:03 +0200 Subject: [PATCH 07/69] Bump ovoenergy to 2.0.1 (#147112) --- homeassistant/components/ovo_energy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/requirements.py | 5 ----- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index af4a313206e..0fc90808bc9 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"], - "requirements": ["ovoenergy==2.0.0"] + "requirements": ["ovoenergy==2.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 28ee85d6565..f6968cc5317 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1632,7 +1632,7 @@ orvibo==1.1.2 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==2.0.0 +ovoenergy==2.0.1 # homeassistant.components.p1_monitor p1monitor==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcc354338e5..666d43f3149 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ oralb-ble==0.17.6 ourgroceries==1.5.4 # homeassistant.components.ovo_energy -ovoenergy==2.0.0 +ovoenergy==2.0.1 # homeassistant.components.p1_monitor p1monitor==3.1.0 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 48066ff6bf0..0576a5b9b6a 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -245,11 +245,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # opower > arrow > types-python-dateutil "arrow": {"types-python-dateutil"} }, - "ovo_energy": { - # https://github.com/timmo001/ovoenergy/issues/132 - # ovoenergy > incremental > setuptools - "incremental": {"setuptools"} - }, "pi_hole": {"hole": {"async-timeout"}}, "pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}}, "remote_rpi_gpio": { From 956f726ef3f9f5fde51152abe3d8ed9d4f68c9ce Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:20:29 +0200 Subject: [PATCH 08/69] Bump uiprotect to version 7.14.0 (#147102) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3c32935a995..f99d910adf9 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.13.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.14.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index f6968cc5317..63d662c1139 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.13.0 +uiprotect==7.14.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 666d43f3149..13906014d2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.13.0 +uiprotect==7.14.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 875d81cab2977bec2793a14f11028125dd84752e Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Thu, 19 Jun 2025 12:04:59 +0200 Subject: [PATCH 09/69] update pyHomee to v1.2.9 (#147094) --- homeassistant/components/homee/__init__.py | 2 +- homeassistant/components/homee/entity.py | 5 +++-- homeassistant/components/homee/lock.py | 8 ++++++-- homeassistant/components/homee/manifest.json | 2 +- homeassistant/components/homee/switch.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homee/__init__.py b/homeassistant/components/homee/__init__.py index e9eb1d86f02..0f90752733d 100644 --- a/homeassistant/components/homee/__init__.py +++ b/homeassistant/components/homee/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo entry.runtime_data = homee entry.async_on_unload(homee.disconnect) - def _connection_update_callback(connected: bool) -> None: + async def _connection_update_callback(connected: bool) -> None: """Call when the device is notified of changes.""" if connected: _LOGGER.warning("Reconnected to Homee at %s", entry.data[CONF_HOST]) diff --git a/homeassistant/components/homee/entity.py b/homeassistant/components/homee/entity.py index 4c85f52bb28..d8344c4226a 100644 --- a/homeassistant/components/homee/entity.py +++ b/homeassistant/components/homee/entity.py @@ -28,6 +28,7 @@ class HomeeEntity(Entity): self._entry = entry node = entry.runtime_data.get_node_by_id(attribute.node_id) # Homee hub itself has node-id -1 + assert node is not None if node.id == -1: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.runtime_data.settings.uid)}, @@ -79,7 +80,7 @@ class HomeeEntity(Entity): def _on_node_updated(self, attribute: HomeeAttribute) -> None: self.schedule_update_ha_state() - def _on_connection_changed(self, connected: bool) -> None: + async def _on_connection_changed(self, connected: bool) -> None: self._host_connected = connected self.schedule_update_ha_state() @@ -166,6 +167,6 @@ class HomeeNodeEntity(Entity): def _on_node_updated(self, node: HomeeNode) -> None: self.schedule_update_ha_state() - def _on_connection_changed(self, connected: bool) -> None: + async def _on_connection_changed(self, connected: bool) -> None: self._host_connected = connected self.schedule_update_ha_state() diff --git a/homeassistant/components/homee/lock.py b/homeassistant/components/homee/lock.py index 4cfc34e11fe..8b3bf58040d 100644 --- a/homeassistant/components/homee/lock.py +++ b/homeassistant/components/homee/lock.py @@ -58,9 +58,13 @@ class HomeeLock(HomeeEntity, LockEntity): AttributeChangedBy, self._attribute.changed_by ) if self._attribute.changed_by == AttributeChangedBy.USER: - changed_id = self._entry.runtime_data.get_user_by_id( + user = self._entry.runtime_data.get_user_by_id( self._attribute.changed_by_id - ).username + ) + if user is not None: + changed_id = user.username + else: + changed_id = "Unknown" return f"{changed_by_name}-{changed_id}" diff --git a/homeassistant/components/homee/manifest.json b/homeassistant/components/homee/manifest.json index 3c2a99c30dc..16ee6085439 100644 --- a/homeassistant/components/homee/manifest.json +++ b/homeassistant/components/homee/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["homee"], "quality_scale": "bronze", - "requirements": ["pyHomee==1.2.8"] + "requirements": ["pyHomee==1.2.9"] } diff --git a/homeassistant/components/homee/switch.py b/homeassistant/components/homee/switch.py index 041b96963f1..5e87a1b4002 100644 --- a/homeassistant/components/homee/switch.py +++ b/homeassistant/components/homee/switch.py @@ -28,6 +28,7 @@ def get_device_class( ) -> SwitchDeviceClass: """Check device class of Switch according to node profile.""" node = config_entry.runtime_data.get_node_by_id(attribute.node_id) + assert node is not None if node.profile in [ NodeProfile.ON_OFF_PLUG, NodeProfile.METERING_PLUG, diff --git a/requirements_all.txt b/requirements_all.txt index 63d662c1139..fa23a9566fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1798,7 +1798,7 @@ pyEmby==1.10 pyHik==0.3.2 # homeassistant.components.homee -pyHomee==1.2.8 +pyHomee==1.2.9 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13906014d2e..7188e1f1395 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1509,7 +1509,7 @@ pyDuotecno==2024.10.1 pyElectra==1.2.4 # homeassistant.components.homee -pyHomee==1.2.8 +pyHomee==1.2.9 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 From 1baba8b88015e04f1a0db996b7ad8f4c5c853826 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Jun 2025 12:36:43 +0200 Subject: [PATCH 10/69] Adjust feature request links in issue reporting (#147130) --- .github/ISSUE_TEMPLATE/bug_report.yml | 5 ++--- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 87fed908c6e..94e876aa3ab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,15 +1,14 @@ name: Report an issue with Home Assistant Core description: Report an issue with Home Assistant Core. -type: Bug body: - type: markdown attributes: value: | This issue form is for reporting bugs only! - If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr]. + If you have a feature or enhancement request, please [request them here instead][fr]. - [fr]: https://community.home-assistant.io/c/feature-requests + [fr]: https://github.com/orgs/home-assistant/discussions - type: textarea validations: required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8a4c7d46708..e14233edfc9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -10,8 +10,8 @@ contact_links: url: https://www.home-assistant.io/help about: We use GitHub for tracking bugs, check our website for resources on getting help. - name: Feature Request - url: https://community.home-assistant.io/c/feature-requests - about: Please use our Community Forum for making feature requests. + url: https://github.com/orgs/home-assistant/discussions + about: Please use this link to request new features or enhancements to existing features. - name: I'm unsure where to go url: https://www.home-assistant.io/join-chat about: If you are unsure where to go, then joining our chat is recommended; Just ask! From 77dca49c759e8ec764a9997c56f3d9738f41bad2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 19 Jun 2025 12:49:10 +0200 Subject: [PATCH 11/69] Fix pylint plugin for vacuum entity (#146467) * Clean out legacy VacuumEntity from pylint plugins * Fix * Fix pylint for vacuum * More fixes * Revert partial * Add back state --- homeassistant/components/lg_thinq/vacuum.py | 3 +- pylint/plugins/hass_enforce_class_module.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 77 ++++----------------- tests/pylint/test_enforce_type_hints.py | 10 +-- 4 files changed, 20 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/lg_thinq/vacuum.py b/homeassistant/components/lg_thinq/vacuum.py index 6cf2a9086b1..6b98b6d8f11 100644 --- a/homeassistant/components/lg_thinq/vacuum.py +++ b/homeassistant/components/lg_thinq/vacuum.py @@ -4,6 +4,7 @@ from __future__ import annotations from enum import StrEnum import logging +from typing import Any from thinqconnect import DeviceType from thinqconnect.integration import ExtendedProperty @@ -154,7 +155,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity): ) ) - async def async_return_to_base(self, **kwargs) -> None: + async def async_return_to_base(self, **kwargs: Any) -> None: """Return device to dock.""" _LOGGER.debug( "[%s:%s] async_return_to_base", diff --git a/pylint/plugins/hass_enforce_class_module.py b/pylint/plugins/hass_enforce_class_module.py index cc7b33d9946..41c07819fe8 100644 --- a/pylint/plugins/hass_enforce_class_module.py +++ b/pylint/plugins/hass_enforce_class_module.py @@ -70,7 +70,7 @@ _MODULES: dict[str, set[str]] = { "todo": {"TodoListEntity"}, "tts": {"TextToSpeechEntity"}, "update": {"UpdateEntity", "UpdateEntityDescription"}, - "vacuum": {"StateVacuumEntity", "VacuumEntity", "VacuumEntityDescription"}, + "vacuum": {"StateVacuumEntity", "VacuumEntityDescription"}, "wake_word": {"WakeWordDetectionEntity"}, "water_heater": {"WaterHeaterEntity"}, "weather": { diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 45a3e41f91a..0760cd33821 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2789,12 +2789,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { matches=_RESTORE_ENTITY_MATCH, ), ClassTypeHintMatch( - base_class="ToggleEntity", - matches=_TOGGLE_ENTITY_MATCH, - ), - ClassTypeHintMatch( - base_class="_BaseVacuum", + base_class="StateVacuumEntity", matches=[ + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), TypeHintMatch( function_name="battery_level", return_type=["int", None], @@ -2821,6 +2821,16 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { return_type=None, has_async_counterpart=True, ), + TypeHintMatch( + function_name="start", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="pause", + return_type=None, + has_async_counterpart=True, + ), TypeHintMatch( function_name="return_to_base", kwargs_type="Any", @@ -2860,63 +2870,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), - ClassTypeHintMatch( - base_class="VacuumEntity", - matches=[ - TypeHintMatch( - function_name="status", - return_type=["str", None], - ), - TypeHintMatch( - function_name="start_pause", - kwargs_type="Any", - return_type=None, - has_async_counterpart=True, - ), - TypeHintMatch( - function_name="async_pause", - return_type=None, - ), - TypeHintMatch( - function_name="async_start", - return_type=None, - ), - ], - ), - ClassTypeHintMatch( - base_class="StateVacuumEntity", - matches=[ - TypeHintMatch( - function_name="state", - return_type=["str", None], - ), - TypeHintMatch( - function_name="start", - return_type=None, - has_async_counterpart=True, - ), - TypeHintMatch( - function_name="pause", - return_type=None, - has_async_counterpart=True, - ), - TypeHintMatch( - function_name="async_turn_on", - kwargs_type="Any", - return_type=None, - ), - TypeHintMatch( - function_name="async_turn_off", - kwargs_type="Any", - return_type=None, - ), - TypeHintMatch( - function_name="async_toggle", - kwargs_type="Any", - return_type=None, - ), - ], - ), ], "water_heater": [ ClassTypeHintMatch( diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 9179a545256..ae426b13fcb 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1161,17 +1161,11 @@ def test_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) - class Entity(): pass - class ToggleEntity(Entity): - pass - - class _BaseVacuum(Entity): - pass - - class VacuumEntity(_BaseVacuum, ToggleEntity): + class StateVacuumEntity(Entity): pass class MyVacuum( #@ - VacuumEntity + StateVacuumEntity ): def send_command( self, From 5bc2e271d2b639000f34bb7aed3bbe2fec590c02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Jun 2025 12:52:01 +0200 Subject: [PATCH 12/69] Re-raise annotated_yaml.YAMLException as HomeAssistantError (#147129) * Re-raise annotated_yaml.YAMLException as HomeAssistantError * Fix comment --- homeassistant/util/yaml/loader.py | 18 +++++++++++------- tests/util/yaml/test_init.py | 4 ++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 1f8338a1ff7..0b5a9ca3c0e 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -6,7 +6,7 @@ from io import StringIO import os from typing import TextIO -from annotatedyaml import YAMLException, YamlTypeError +import annotatedyaml from annotatedyaml.loader import ( HAS_C_LOADER, JSON_TYPE, @@ -35,6 +35,10 @@ __all__ = [ ] +class YamlTypeError(HomeAssistantError): + """Raised by load_yaml_dict if top level data is not a dict.""" + + def load_yaml( fname: str | os.PathLike[str], secrets: Secrets | None = None ) -> JSON_TYPE | None: @@ -45,7 +49,7 @@ def load_yaml( """ try: return load_annotated_yaml(fname, secrets) - except YAMLException as exc: + except annotatedyaml.YAMLException as exc: raise HomeAssistantError(str(exc)) from exc @@ -59,9 +63,9 @@ def load_yaml_dict( """ try: return load_annotated_yaml_dict(fname, secrets) - except YamlTypeError: - raise - except YAMLException as exc: + except annotatedyaml.YamlTypeError as exc: + raise YamlTypeError(str(exc)) from exc + except annotatedyaml.YAMLException as exc: raise HomeAssistantError(str(exc)) from exc @@ -71,7 +75,7 @@ def parse_yaml( """Parse YAML with the fastest available loader.""" try: return parse_annotated_yaml(content, secrets) - except YAMLException as exc: + except annotatedyaml.YAMLException as exc: raise HomeAssistantError(str(exc)) from exc @@ -79,5 +83,5 @@ def secret_yaml(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE: """Load secrets and embed it into the configuration YAML.""" try: return annotated_secret_yaml(loader, node) - except YAMLException as exc: + except annotatedyaml.YAMLException as exc: raise HomeAssistantError(str(exc)) from exc diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index dacbd2c1247..94c3dd204f7 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -559,6 +559,10 @@ def test_load_yaml_dict(expected_data: Any) -> None: @pytest.mark.usefixtures("try_both_loaders", "mock_hass_config_yaml") def test_load_yaml_dict_fail() -> None: """Test item without a key.""" + # Make sure we raise a subclass of HomeAssistantError, not + # annotated_yaml.YAMLException + assert issubclass(yaml_loader.YamlTypeError, HomeAssistantError) + with pytest.raises(yaml_loader.YamlTypeError): yaml_loader.load_yaml_dict(YAML_CONFIG_FILE) From 0db652080282142f88c536e389fc23aa6cdad8ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Jun 2025 12:59:35 +0200 Subject: [PATCH 13/69] Add comment in helpers.llm.ActionTool explaining limitations (#147116) --- homeassistant/helpers/llm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 51b5510495f..1e4abb07ddb 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -901,6 +901,12 @@ class ActionTool(Tool): self._domain = domain self._action = action self.name = f"{domain}.{action}" + # Note: _get_cached_action_parameters only works for services which + # add their description directly to the service description cache. + # This is not the case for most services, but it is for scripts. + # If we want to use `ActionTool` for services other than scripts, we + # need to add a coroutine function to fetch the non-cached description + # and schema. self.description, self.parameters = _get_cached_action_parameters( hass, domain, action ) From 513045e4894b47de8fe3a8840f3eec964ad58a74 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 13:07:42 +0200 Subject: [PATCH 14/69] Update pytest warnings filter (#147132) --- pyproject.toml | 55 ++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c4cfe7a8593..83782631191 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -527,15 +527,11 @@ filterwarnings = [ # -- fixed, waiting for release / update # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 - "ignore:invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", + "ignore:.*invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", # https://github.com/DataDog/datadogpy/pull/566/files - >=0.37.0 "ignore:pkg_resources is deprecated as an API:UserWarning:datadog.util.compat", # https://github.com/httplib2/httplib2/pull/226 - >=0.21.0 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:httplib2", - # https://github.com/majuss/lupupy/pull/15 - >0.3.2 - "ignore:\"is not\" with 'str' literal. Did you mean \"!=\"?:SyntaxWarning:.*lupupy.devices.alarm", - # https://github.com/nextcord/nextcord/pull/1095 - >=3.0.0 - "ignore:pkg_resources is deprecated as an API:UserWarning:nextcord.health_check", # https://github.com/vacanza/python-holidays/discussions/1800 - >1.0.0 "ignore::DeprecationWarning:holidays", # https://github.com/ReactiveX/RxPY/pull/716 - >4.0.4 @@ -549,7 +545,7 @@ filterwarnings = [ # https://github.com/rytilahti/python-miio/pull/1993 - >0.6.0.dev0 "ignore:functools.partial will be a method descriptor in future Python versions; wrap it in enum.member\\(\\) if you want to preserve the old behavior:FutureWarning:miio.miot_device", # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 - "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", + "ignore:.*invalid escape sequence:SyntaxWarning:.*stringcase", # https://github.com/xchwarze/samsung-tv-ws-api/pull/151 - >2.7.2 - 2024-12-06 # wrong stacklevel in aiohttp "ignore:verify_ssl is deprecated, use ssl=False instead:DeprecationWarning:aiohttp.client", @@ -572,47 +568,53 @@ filterwarnings = [ # https://pypi.org/project/pyeconet/ - v0.1.28 - 2025-02-15 # https://github.com/w1ll1am23/pyeconet/blob/v0.1.28/src/pyeconet/api.py#L38 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:pyeconet.api", - # https://github.com/thecynic/pylutron - v0.2.16 - 2024-10-22 + # https://github.com/thecynic/pylutron - v0.2.18 - 2025-04-15 "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", # https://pypi.org/project/PyMetEireann/ - v2024.11.0 - 2024-11-23 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteireann", # https://github.com/pschmitt/pynuki/blob/1.6.3/pynuki/utils.py#L21 - v1.6.3 - 2024-02-24 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pynuki.utils", - # https://github.com/lextudio/pysnmp/blob/v7.1.17/pysnmp/smi/compiler.py#L23-L31 - v7.1.17 - 2025-03-19 + # https://github.com/lextudio/pysnmp/blob/v7.1.21/pysnmp/smi/compiler.py#L23-L31 - v7.1.21 - 2025-06-19 "ignore:smiV1Relaxed is deprecated. Please use smi_v1_relaxed instead:DeprecationWarning:pysnmp.smi.compiler", "ignore:getReadersFromUrls is deprecated. Please use get_readers_from_urls instead:DeprecationWarning:pysnmp.smi.compiler", - # https://github.com/Python-roborock/python-roborock/issues/305 - 2.18.0 - 2025-04-06 + # https://github.com/Python-roborock/python-roborock/issues/305 - 2.19.0 - 2025-05-13 "ignore:Callback API version 1 is deprecated, update to latest version:DeprecationWarning:roborock.cloud_api", - # https://github.com/Teslemetry/python-tesla-fleet-api - v1.1.1 - 2025-05-29 - "ignore:Protobuf gencode .* exactly one major version older than the runtime version 6.* at (car_server|common|errors|keys|managed_charging|signatures|universal_message|vcsec|vehicle):UserWarning:google.protobuf.runtime_version", # https://github.com/briis/pyweatherflowudp/blob/v1.4.5/pyweatherflowudp/const.py#L20 - v1.4.5 - 2023-10-10 "ignore:This function will be removed in future versions of pint:DeprecationWarning:pyweatherflowudp.const", # New in aiohttp - v3.9.0 "ignore:It is recommended to use web.AppKey instances for keys:UserWarning:(homeassistant|tests|aiohttp_cors)", # - SyntaxWarnings # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 - "ignore:invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", + "ignore:.*invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aprslib.parsing.common", # https://pypi.org/project/panasonic-viera/ - v0.4.2 - 2024-04-24 # https://github.com/florianholzapfel/panasonic-viera/blob/0.4.2/panasonic_viera/__init__.py#L789 - "ignore:invalid escape sequence:SyntaxWarning:.*panasonic_viera", + "ignore:.*invalid escape sequence:SyntaxWarning:.*panasonic_viera", # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 # https://github.com/koolsb/pyblackbird/pull/9 -> closed - "ignore:invalid escape sequence:SyntaxWarning:.*pyblackbird", + "ignore:.*invalid escape sequence:SyntaxWarning:.*pyblackbird", # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 - "ignore:invalid escape sequence:SyntaxWarning:.*pyws66i", + "ignore:.*invalid escape sequence:SyntaxWarning:.*pyws66i", # https://pypi.org/project/sanix/ - v1.0.6 - 2024-05-01 # https://github.com/tomaszsluszniak/sanix_py/blob/v1.0.6/sanix/__init__.py#L42 - "ignore:invalid escape sequence:SyntaxWarning:.*sanix", + "ignore:.*invalid escape sequence:SyntaxWarning:.*sanix", # https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18 - "ignore:invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # codespell:ignore thirdparty + "ignore:.*invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # codespell:ignore thirdparty # - pkg_resources + # https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20 + "ignore:pkg_resources is deprecated as an API:UserWarning:aiomusiccast", # https://github.com/eavanvalkenburg/pysiaalarm/blob/v3.1.1/src/pysiaalarm/data/data.py#L7 - v3.1.1 - 2023-04-17 "ignore:pkg_resources is deprecated as an API:UserWarning:pysiaalarm.data.data", - # https://pypi.org/project/pybotvac/ - v0.0.26 - 2025-02-26 + # https://pypi.org/project/pybotvac/ - v0.0.28 - 2025-06-11 "ignore:pkg_resources is deprecated as an API:UserWarning:pybotvac.version", # https://github.com/home-assistant-ecosystem/python-mystrom/blob/2.2.0/pymystrom/__init__.py#L10 - v2.2.0 - 2023-05-21 "ignore:pkg_resources is deprecated as an API:UserWarning:pymystrom", + # - SyntaxWarning - is with literal + # https://github.com/majuss/lupupy/pull/15 - >0.3.2 + # https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16 + # https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16 + # https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19 + "ignore:\"is.*\" with '.*' literal:SyntaxWarning:importlib._bootstrap", # -- New in Python 3.13 # https://github.com/kurtmckee/feedparser/pull/389 - >6.0.11 @@ -628,11 +630,11 @@ filterwarnings = [ # -- Websockets 14.1 # https://websockets.readthedocs.io/en/stable/howto/upgrade.html "ignore:websockets.legacy is deprecated:DeprecationWarning:websockets.legacy", - # https://github.com/bluecurrent/HomeAssistantAPI + # https://github.com/bluecurrent/HomeAssistantAPI/pull/19 - >=1.2.4 "ignore:websockets.client.connect is deprecated:DeprecationWarning:bluecurrent_api.websocket", "ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:bluecurrent_api.websocket", "ignore:websockets.exceptions.InvalidStatusCode is deprecated:DeprecationWarning:bluecurrent_api.websocket", - # https://github.com/graphql-python/gql + # https://github.com/graphql-python/gql/pull/543 - >=4.0.0a0 "ignore:websockets.client.WebSocketClientProtocol is deprecated:DeprecationWarning:gql.transport.websockets_base", # -- unmaintained projects, last release about 2+ years @@ -655,21 +657,16 @@ filterwarnings = [ "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:lomond.session", # https://pypi.org/project/oauth2client/ - v4.1.3 - 2018-09-07 (archived) "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:oauth2client.client", - # https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16 - "ignore:\"is not\" with 'int' literal. Did you mean \"!=\"?:SyntaxWarning:.*opuslib.api.decoder", # https://pypi.org/project/pilight/ - v0.1.1 - 2016-10-19 "ignore:pkg_resources is deprecated as an API:UserWarning:pilight", # https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16 - "ignore:invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery", - "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*plumlightpad.(lightpad|logicalload)", + "ignore:.*invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery", # https://pypi.org/project/pure-python-adb/ - v0.3.0.dev0 - 2020-08-05 - "ignore:invalid escape sequence:SyntaxWarning:.*ppadb", + "ignore:.*invalid escape sequence:SyntaxWarning:.*ppadb", # https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10 - "ignore:invalid escape sequence:SyntaxWarning:.*pydub.utils", - # https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19 - "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*pyiss", + "ignore:.*invalid escape sequence:SyntaxWarning:.*pydub.utils", # https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21 - "ignore:invalid escape sequence:SyntaxWarning:.*pypasser.utils", + "ignore:.*invalid escape sequence:SyntaxWarning:.*pypasser.utils", # https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19 "ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_", "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_", From c602a0e279ded182f9a374c7a39e29b279a8f901 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 19 Jun 2025 13:14:42 +0200 Subject: [PATCH 15/69] Deprecated hass.http.register_static_path now raises error (#147039) --- homeassistant/components/http/__init__.py | 8 +++++--- tests/components/http/test_cors.py | 5 ++++- tests/components/http/test_init.py | 13 +++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 8ee27039441..2c4b67e6c99 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -511,12 +511,14 @@ class HomeAssistantHTTP: ) -> None: """Register a folder or file to serve as a static path.""" frame.report_usage( - "calls hass.http.register_static_path which is deprecated because " - "it does blocking I/O in the event loop, instead " + "calls hass.http.register_static_path which " + "does blocking I/O in the event loop, instead " "call `await hass.http.async_register_static_paths(" f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`', exclude_integrations={"http"}, - core_behavior=frame.ReportBehavior.LOG, + core_behavior=frame.ReportBehavior.ERROR, + core_integration_behavior=frame.ReportBehavior.ERROR, + custom_integration_behavior=frame.ReportBehavior.ERROR, breaks_in_ha_version="2025.7", ) configs = [StaticPathConfig(url_path, path, cache_headers)] diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 0581c7bac2a..bddd66a7e81 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -16,6 +16,7 @@ from aiohttp.hdrs import ( from aiohttp.test_utils import TestClient import pytest +from homeassistant.components.http import StaticPathConfig from homeassistant.components.http.cors import setup_cors from homeassistant.core import HomeAssistant from homeassistant.helpers.http import KEY_ALLOW_CONFIGURED_CORS, HomeAssistantView @@ -157,7 +158,9 @@ async def test_cors_on_static_files( assert await async_setup_component( hass, "frontend", {"http": {"cors_allowed_origins": ["http://www.example.com"]}} ) - hass.http.register_static_path("/something", str(Path(__file__).parent)) + await hass.http.async_register_static_paths( + [StaticPathConfig("/something", str(Path(__file__).parent))] + ) client = await hass_client() resp = await client.options( diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 2937e673946..7858bbc123d 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -530,17 +530,14 @@ async def test_register_static_paths( """Test registering a static path with old api.""" assert await async_setup_component(hass, "frontend", {}) path = str(Path(__file__).parent) - hass.http.register_static_path("/something", path) - client = await hass_client() - resp = await client.get("/something/__init__.py") - assert resp.status == HTTPStatus.OK - assert ( + match_error = ( "Detected code that calls hass.http.register_static_path " - "which is deprecated because it does blocking I/O in the " - "event loop, instead call " + "which does blocking I/O in the event loop, instead call " "`await hass.http.async_register_static_paths" - ) in caplog.text + ) + with pytest.raises(RuntimeError, match=match_error): + hass.http.register_static_path("/something", path) async def test_ssl_issue_if_no_urls_configured( From 31eec6f471c230626898d36d6b4a480be58d25e9 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 19 Jun 2025 13:36:40 +0200 Subject: [PATCH 16/69] Add missing hyphen to "mains-powered" and "battery-powered" in `zha` (#147128) Add missing hyphen to "mains-powered" and "battery-powered" --- homeassistant/components/zha/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 95bf339f7d9..1327a78b0b3 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -182,9 +182,9 @@ "group_members_assume_state": "Group members assume state of group", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", - "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", - "enable_mains_startup_polling": "Refresh state for mains powered devices on startup", - "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)" + "consider_unavailable_mains": "Consider mains-powered devices unavailable after (seconds)", + "enable_mains_startup_polling": "Refresh state for mains-powered devices on startup", + "consider_unavailable_battery": "Consider battery-powered devices unavailable after (seconds)" }, "zha_alarm_options": { "title": "Alarm control panel options", From 7a5c088149b5ac2148106af9eaf209ac8457cc4f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:42:30 +0200 Subject: [PATCH 17/69] [ci] Bump cache key version (#147148) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5a5172f513f..19cc8bd3af7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 2 + CACHE_VERSION: 3 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.7" From da3d8a6332560e2e80593d4a0e260bf3085fe1f8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 19 Jun 2025 17:56:47 +0200 Subject: [PATCH 18/69] Improve advanced Z-Wave battery discovery (#147127) --- .../components/zwave_js/binary_sensor.py | 34 +- homeassistant/components/zwave_js/const.py | 5 +- .../components/zwave_js/discovery.py | 31 +- .../zwave_js/discovery_data_template.py | 32 +- homeassistant/components/zwave_js/sensor.py | 47 +- tests/components/zwave_js/common.py | 1 - tests/components/zwave_js/conftest.py | 14 + .../zwave_js/fixtures/ring_keypad_state.json | 7543 +++++++++++++++++ .../components/zwave_js/test_binary_sensor.py | 52 +- tests/components/zwave_js/test_sensor.py | 93 +- 10 files changed, 7825 insertions(+), 27 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/ring_keypad_state.json diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 1439aa0ca0f..d70690ace31 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -318,12 +318,37 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { # Mappings for boolean sensors -BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = { - CommandClass.BATTERY: BinarySensorEntityDescription( - key=str(CommandClass.BATTERY), +BOOLEAN_SENSOR_MAPPINGS: dict[tuple[int, int | str], BinarySensorEntityDescription] = { + (CommandClass.BATTERY, "backup"): BinarySensorEntityDescription( + key="battery_backup", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "disconnected"): BinarySensorEntityDescription( + key="battery_disconnected", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "isLow"): BinarySensorEntityDescription( + key="battery_is_low", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ), + (CommandClass.BATTERY, "lowFluid"): BinarySensorEntityDescription( + key="battery_low_fluid", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "overheating"): BinarySensorEntityDescription( + key="battery_overheating", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + (CommandClass.BATTERY, "rechargeable"): BinarySensorEntityDescription( + key="battery_rechargeable", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), } @@ -432,8 +457,9 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): # Entity class attributes self._attr_name = self.generate_name(include_value_name=True) + primary_value = self.info.primary_value if description := BOOLEAN_SENSOR_MAPPINGS.get( - self.info.primary_value.command_class + (primary_value.command_class, primary_value.property_) ): self.entity_description = description diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 3d626710d52..a99e9fd0113 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -139,7 +139,10 @@ ATTR_TWIST_ASSIST = "twist_assist" ADDON_SLUG = "core_zwave_js" # Sensor entity description constants -ENTITY_DESC_KEY_BATTERY = "battery" +ENTITY_DESC_KEY_BATTERY_LEVEL = "battery_level" +ENTITY_DESC_KEY_BATTERY_LIST_STATE = "battery_list_state" +ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY = "battery_maximum_capacity" +ENTITY_DESC_KEY_BATTERY_TEMPERATURE = "battery_temperature" ENTITY_DESC_KEY_CURRENT = "current" ENTITY_DESC_KEY_VOLTAGE = "voltage" ENTITY_DESC_KEY_ENERGY_MEASUREMENT = "energy_measurement" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 4e9a3321beb..92233dd2e77 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -913,7 +913,6 @@ DISCOVERY_SCHEMAS = [ hint="numeric_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={ - CommandClass.BATTERY, CommandClass.ENERGY_PRODUCTION, CommandClass.SENSOR_ALARM, CommandClass.SENSOR_MULTILEVEL, @@ -922,6 +921,36 @@ DISCOVERY_SCHEMAS = [ ), data_template=NumericSensorDataTemplate(), ), + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BATTERY}, + type={ValueType.NUMBER}, + property={"level", "maximumCapacity"}, + ), + data_template=NumericSensorDataTemplate(), + ), + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="numeric_sensor", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BATTERY}, + type={ValueType.NUMBER}, + property={"temperature"}, + ), + data_template=NumericSensorDataTemplate(), + ), + ZWaveDiscoverySchema( + platform=Platform.SENSOR, + hint="list", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.BATTERY}, + type={ValueType.NUMBER}, + property={"chargingStatus", "rechargeOrReplace"}, + ), + data_template=NumericSensorDataTemplate(), + ), ZWaveDiscoverySchema( platform=Platform.SENSOR, hint="numeric_sensor", diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index e619c6afc7c..731a786d226 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -133,7 +133,10 @@ from homeassistant.const import ( ) from .const import ( - ENTITY_DESC_KEY_BATTERY, + ENTITY_DESC_KEY_BATTERY_LEVEL, + ENTITY_DESC_KEY_BATTERY_LIST_STATE, + ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, ENTITY_DESC_KEY_CO, ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, @@ -380,8 +383,31 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData: """Resolve helper class data for a discovered value.""" - if value.command_class == CommandClass.BATTERY: - return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE) + if value.command_class == CommandClass.BATTERY and value.property_ == "level": + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE + ) + if value.command_class == CommandClass.BATTERY and value.property_ in ( + "chargingStatus", + "rechargeOrReplace", + ): + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_LIST_STATE, None + ) + if ( + value.command_class == CommandClass.BATTERY + and value.property_ == "maximumCapacity" + ): + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE + ) + if ( + value.command_class == CommandClass.BATTERY + and value.property_ == "temperature" + ): + return NumericSensorDataTemplateData( + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, UnitOfTemperature.CELSIUS + ) if value.command_class == CommandClass.METER: try: diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 4db14d003b1..05fa785760b 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -58,7 +58,10 @@ from .const import ( ATTR_VALUE, DATA_CLIENT, DOMAIN, - ENTITY_DESC_KEY_BATTERY, + ENTITY_DESC_KEY_BATTERY_LEVEL, + ENTITY_DESC_KEY_BATTERY_LIST_STATE, + ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, ENTITY_DESC_KEY_CO, ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, @@ -95,17 +98,33 @@ from .migrate import async_migrate_statistics_sensors PARALLEL_UPDATES = 0 -# These descriptions should include device class. -ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ - tuple[str, str], SensorEntityDescription -] = { - (ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription( - key=ENTITY_DESC_KEY_BATTERY, +# These descriptions should have a non None unit of measurement. +ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription] = { + (ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE): SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_LEVEL, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), + (ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE): SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + ), + ( + ENTITY_DESC_KEY_BATTERY_TEMPERATURE, + UnitOfTemperature.CELSIUS, + ): SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_TEMPERATURE, + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + entity_registry_enabled_default=False, + ), (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription( key=ENTITY_DESC_KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, @@ -285,8 +304,14 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ ), } -# These descriptions are without device class. +# These descriptions are without unit of measurement. ENTITY_DESCRIPTION_KEY_MAP = { + ENTITY_DESC_KEY_BATTERY_LIST_STATE: SensorEntityDescription( + key=ENTITY_DESC_KEY_BATTERY_LIST_STATE, + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), ENTITY_DESC_KEY_CO: SensorEntityDescription( key=ENTITY_DESC_KEY_CO, state_class=SensorStateClass.MEASUREMENT, @@ -538,7 +563,7 @@ def get_entity_description( """Return the entity description for the given data.""" data_description_key = data.entity_description_key or "" data_unit = data.unit_of_measurement or "" - return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get( + return ENTITY_DESCRIPTION_KEY_UNIT_MAP.get( (data_description_key, data_unit), ENTITY_DESCRIPTION_KEY_MAP.get( data_description_key, @@ -588,6 +613,10 @@ async def async_setup_entry( entities.append( ZWaveListSensor(config_entry, driver, info, entity_description) ) + elif info.platform_hint == "list": + entities.append( + ZWaveListSensor(config_entry, driver, info, entity_description) + ) elif info.platform_hint == "config_parameter": entities.append( ZWaveConfigParameterSensor( diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 64bc981de11..578eeab5ec7 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -21,7 +21,6 @@ ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" VOLTAGE_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_3" CURRENT_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_4" SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports" -LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_detection" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e0485ced091..25f40e4418d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -199,6 +199,12 @@ def climate_heatit_z_trm3_no_value_state_fixture() -> dict[str, Any]: return load_json_object_fixture("climate_heatit_z_trm3_no_value_state.json", DOMAIN) +@pytest.fixture(name="ring_keypad_state", scope="package") +def ring_keypad_state_fixture() -> dict[str, Any]: + """Load the Ring keypad state fixture data.""" + return load_json_object_fixture("ring_keypad_state.json", DOMAIN) + + @pytest.fixture(name="nortek_thermostat_state", scope="package") def nortek_thermostat_state_fixture() -> dict[str, Any]: """Load the nortek thermostat node state fixture data.""" @@ -876,6 +882,14 @@ def nortek_thermostat_removed_event_fixture(client) -> Node: return Event("node removed", event_data) +@pytest.fixture(name="ring_keypad") +def ring_keypad_fixture(client: MagicMock, ring_keypad_state: NodeDataType) -> Node: + """Mock a Ring keypad node.""" + node = Node(client, copy.deepcopy(ring_keypad_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="integration") async def integration_fixture( hass: HomeAssistant, diff --git a/tests/components/zwave_js/fixtures/ring_keypad_state.json b/tests/components/zwave_js/fixtures/ring_keypad_state.json new file mode 100644 index 00000000000..3d003518b6e --- /dev/null +++ b/tests/components/zwave_js/fixtures/ring_keypad_state.json @@ -0,0 +1,7543 @@ +{ + "nodeId": 4, + "index": 0, + "installerIcon": 8193, + "userIcon": 8193, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 838, + "productId": 1025, + "productType": 257, + "firmwareVersion": "1.18.0", + "zwavePlusVersion": 2, + "deviceConfig": { + "filename": "/home/dominic/Repositories/zwavejs2mqtt/store/.config-db/devices/0x0346/keypad_v2.json", + "isEmbedded": true, + "manufacturer": "Ring", + "manufacturerId": 838, + "label": "4AK1SZ", + "description": "Keypad v2", + "devices": [ + { + "productType": 257, + "productId": 769 + }, + { + "productType": 257, + "productId": 1025 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "paramInformation": { + "_map": {} + }, + "compat": { + "disableStrictEntryControlDataValidation": true + }, + "metadata": { + "inclusion": "Classic Inclusion should be used if the controller does not support SmartStart.\n1. Initiate add flow for Security Devices in the Ring mobile application \u2013 Follow the guided add flow instructions provided in the Ring mobile application.\n2. Select add manually and enter the 5-digit DSK PIN found on the package of the Ring Alarm Keypad or the 5-digit DSK PIN found under the QR code on the device.\n3. After powering on the device, press and hold the #1 button for ~3 seconds. Release the button and the device will enter Classic inclusion mode which implements both classic inclusion with a Node Information Frame, and Network Wide Inclusion. During Classic Inclusion mode, the green Connection LED will blink three times followed by a brief pause, repeatedly. When Classic inclusion times-out, the device will blink alternating red and green a few times", + "exclusion": "1. Initiate remove 'Ring Alarm Keypad' flow in the Ring Alarm mobile application \u2013 Select the settings icon from device details page and choose 'Remove Device' to remove the device. This will place the controller into Remove or 'Z-Wave Exclusion' mode.\n2. Locate the pinhole reset button on the back of the device.\n3. With the controller in Remove (Z-Wave Exclusion) mode, use a paper clip or similar object and tap the pinhole button. The device's Connection LED turns on solid red to indicate the device was removed from the network.", + "reset": "Factory Default Instructions\n1. To restore Ring Alarm Keypad to factory default settings, locate the pinhole reset button on the device. This is found on the back of the device after removing the back bracket.\n2. Using a paperclip or similar object, insert it into the pinhole, press and hold the button down for 10 seconds.\n3. The device's Connection icon LED will rapidly blink green continuously for 10 seconds. After about 10 seconds, when the green blinking stops, release the button. The red LED will turn on solid to indicate the device was removed from the network.\nNote\nUse this procedure only in the event that the network primary controller is missing or otherwise inoperable", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/4150/Ring%20Alarm%20Keypad%20Zwave.pdf" + } + }, + "label": "4AK1SZ", + "interviewAttempts": 0, + "isFrequentListening": "250ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 11, + "label": "Secure Keypad" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0346:0x0101:0x0401:1.18.0", + "statistics": { + "commandsTX": 1, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0, + "rtt": 27.5, + "lastSeen": "2025-06-18T11:17:39.315Z", + "rssi": -54, + "lwr": { + "protocolDataRate": 2, + "repeaters": [], + "rssi": -54, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": 2, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2025-06-18T11:17:39.315Z", + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 111, + "commandClassName": "Entry Control", + "property": "keyCacheTimeout", + "propertyName": "keyCacheTimeout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "How long the key cache must wait for additional characters", + "label": "Key cache timeout", + "min": 1, + "max": 30, + "unit": "seconds", + "stateful": true, + "secret": false + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 111, + "commandClassName": "Entry Control", + "property": "keyCacheSize", + "propertyName": "keyCacheSize", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Number of character that must be stored before sending", + "label": "Key cache size", + "min": 4, + "max": 10, + "stateful": true, + "secret": false + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Heartbeat Interval", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heartbeat Interval", + "default": 70, + "min": 1, + "max": 70, + "unit": "minutes", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 70 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Message Retry Attempt Limit", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Message Retry Attempt Limit", + "default": 1, + "min": 0, + "max": 5, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Delay Between Retry Attempts", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Delay Between Retry Attempts", + "default": 5, + "min": 1, + "max": 60, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Announcement Audio Volume", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Announcement Audio Volume", + "default": 7, + "min": 0, + "max": 10, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 7 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Key Tone Volume", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Key Tone Volume", + "default": 6, + "min": 0, + "max": 10, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Siren Volume", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Siren Volume", + "default": 10, + "min": 0, + "max": 10, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Long Press Duration: Emergency Buttons", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Hold time required to capture a long press", + "label": "Long Press Duration: Emergency Buttons", + "default": 3, + "min": 2, + "max": 5, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Long Press Duration: Number Pad", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Hold time required to capture a long press", + "label": "Long Press Duration: Number Pad", + "default": 3, + "min": 2, + "max": 5, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Timeout: Proximity Display", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Timeout: Proximity Display", + "default": 5, + "min": 0, + "max": 30, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Timeout: Display on Button Press", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Timeout: Display on Button Press", + "default": 5, + "min": 0, + "max": 30, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Timeout: Display on Status Change", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Timeout: Display on Status Change", + "default": 5, + "min": 1, + "max": 30, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Brightness: Security Mode", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Brightness: Security Mode", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Brightness: Key Backlight", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Brightness: Key Backlight", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Key Backlight Ambient Light Sensor Level", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Key Backlight Ambient Light Sensor Level", + "default": 20, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Proximity Detection", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Proximity Detection", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "LED Ramp Time", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED Ramp Time", + "default": 50, + "min": 0, + "max": 255, + "unit": "seconds", + "valueSize": 1, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Battery Low Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Low Threshold", + "default": 15, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 30 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Battery Warning Threshold", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Battery Warning Threshold", + "default": 5, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Keypad Language", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Keypad Language", + "default": 30, + "min": 0, + "max": 31, + "states": { + "0": "English", + "2": "French", + "5": "Spanish" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "System Security Mode Blink Duration", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "System Security Mode Blink Duration", + "default": 2, + "min": 1, + "max": 60, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Supervision Report Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Supervision Report Timeout", + "default": 10000, + "min": 500, + "max": 30000, + "unit": "ms", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10000 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "System Security Mode Display", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Allowable range: 1-600", + "label": "System Security Mode Display", + "default": 0, + "min": 0, + "max": 601, + "states": { + "0": "Always off", + "601": "Always on" + }, + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1, + "propertyName": "param023_1", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 1, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2, + "propertyName": "param023_2", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 4, + "propertyName": "param023_4", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 1, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 8, + "propertyName": "param023_8", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 16, + "propertyName": "param023_16", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 32, + "propertyName": "param023_32", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 1, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 64, + "propertyName": "param023_64", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 128, + "propertyName": "param023_128", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 256, + "propertyName": "param023_256", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 512, + "propertyName": "param023_512", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1024, + "propertyName": "param023_1024", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2048, + "propertyName": "param023_2048", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 4096, + "propertyName": "param023_4096", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 8192, + "propertyName": "param023_8192", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 16384, + "propertyName": "param023_16384", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 32768, + "propertyName": "param023_32768", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 65536, + "propertyName": "param023_65536", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 131072, + "propertyName": "param023_131072", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 262144, + "propertyName": "param023_262144", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 524288, + "propertyName": "param023_524288", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1048576, + "propertyName": "param023_1048576", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2097152, + "propertyName": "param023_2097152", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 4194304, + "propertyName": "param023_4194304", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 8388608, + "propertyName": "param023_8388608", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 16777216, + "propertyName": "param023_16777216", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 33554432, + "propertyName": "param023_33554432", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 67108864, + "propertyName": "param023_67108864", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 134217728, + "propertyName": "param023_134217728", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 268435456, + "propertyName": "param023_268435456", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 536870912, + "propertyName": "param023_536870912", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 1073741824, + "propertyName": "param023_1073741824", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyKey": 2147483648, + "propertyName": "param023_2147483648", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "default": 0, + "min": 0, + "max": 1, + "valueSize": 4, + "format": 3, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 24, + "propertyName": "Calibrate Speaker", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Calibrate Speaker", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 26, + "propertyName": "Motion Sensor Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Motion Sensor Timeout", + "default": 3, + "min": 0, + "max": 60, + "unit": "seconds", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 25, + "propertyName": "Z-Wave Sleep Timeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "", + "label": "Z-Wave Sleep Timeout", + "default": 10, + "min": 0, + "max": 15, + "valueSize": 1, + "format": 1, + "isAdvanced": true, + "requiresReInclusion": false, + "allowManualEntry": true, + "isFromConfig": false + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 23, + "propertyName": "Languages Supported Report", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "This parameter reports a bitmask of supported languages", + "label": "Languages Supported Report", + "default": 37, + "min": 0, + "max": 4294967295, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motion sensor status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "8": "Motion detection" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Power status", + "propertyName": "Power Management", + "propertyKeyName": "Power status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Power status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Power has been applied" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyKey": "Software status", + "propertyName": "System", + "propertyKeyName": "Software status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Software status", + "ccSpecific": { + "notificationType": 9 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "4": "System software failure (with failure code)" + }, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Mains status", + "propertyName": "Power Management", + "propertyKeyName": "Mains status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Mains status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "2": "AC mains disconnected", + "3": "AC mains re-connected" + }, + "stateful": true, + "secret": false + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 1025 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 257 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 838 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "disconnected", + "propertyName": "disconnected", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Battery is disconnected", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeOrReplace", + "propertyName": "rechargeOrReplace", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Recharge or replace", + "min": 0, + "max": 255, + "states": { + "0": "No", + "1": "Soon", + "2": "Now" + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "lowFluid", + "propertyName": "lowFluid", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Fluid is low", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "overheating", + "propertyName": "overheating", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Overheating", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "backup", + "propertyName": "backup", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Used as backup", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "rechargeable", + "propertyName": "rechargeable", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Rechargeable", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "chargingStatus", + "propertyName": "chargingStatus", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Charging status", + "min": 0, + "max": 255, + "states": { + "0": "Discharging", + "1": "Charging", + "2": "Maintaining" + }, + "stateful": true, + "secret": false + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "temperature", + "propertyName": "temperature", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Temperature", + "min": -128, + "max": 127, + "unit": "\u00b0C", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "maximumCapacity", + "propertyName": "maximumCapacity", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Maximum capacity", + "min": 0, + "max": 100, + "unit": "%", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["1.18", "1.1"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.12" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 28 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "1.18.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 48 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.12.4" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 48 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "1.18.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.12.4" + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 1, + "propertyName": "0", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0 (default) - Multilevel", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 3, + "propertyName": "0", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0 (default) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 4, + "propertyName": "0", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0 (default) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 5, + "propertyName": "0", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0 (default) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 7, + "propertyName": "0", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0 (default) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 9, + "propertyName": "0", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0 (default) - Sound level", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 8, + "propertyName": "0", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0 (default) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 0, + "propertyKey": 6, + "propertyName": "0", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0 (default) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 0, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Timeout", + "stateful": true, + "secret": false + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 1, + "propertyName": "Ready", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x03 (Ready) - Multilevel", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 3, + "propertyName": "Ready", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x03 (Ready) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 4, + "propertyName": "Ready", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x03 (Ready) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 5, + "propertyName": "Ready", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x03 (Ready) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 7, + "propertyName": "Ready", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x03 (Ready) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 9, + "propertyName": "Ready", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x03 (Ready) - Sound level", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 8, + "propertyName": "Ready", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x03 (Ready) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 3, + "propertyKey": 6, + "propertyName": "Ready", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x03 (Ready) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 3, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 1, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x02 (Not armed / disarmed) - Multilevel", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 3, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x02 (Not armed / disarmed) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 4, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x02 (Not armed / disarmed) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 5, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x02 (Not armed / disarmed) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 7, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x02 (Not armed / disarmed) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 9, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x02 (Not armed / disarmed) - Sound level", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 8, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x02 (Not armed / disarmed) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 2, + "propertyKey": 6, + "propertyName": "Not armed / disarmed", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x02 (Not armed / disarmed) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 2, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 1, + "propertyName": "Code not accepted", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x09 (Code not accepted) - Multilevel", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 3, + "propertyName": "Code not accepted", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x09 (Code not accepted) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 4, + "propertyName": "Code not accepted", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x09 (Code not accepted) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 5, + "propertyName": "Code not accepted", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x09 (Code not accepted) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 7, + "propertyName": "Code not accepted", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x09 (Code not accepted) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 9, + "propertyName": "Code not accepted", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x09 (Code not accepted) - Sound level", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 8, + "propertyName": "Code not accepted", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x09 (Code not accepted) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 9, + "propertyKey": 6, + "propertyName": "Code not accepted", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x09 (Code not accepted) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 9, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 1, + "propertyName": "Armed Stay", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0a (Armed Stay) - Multilevel", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 3, + "propertyName": "Armed Stay", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0a (Armed Stay) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 4, + "propertyName": "Armed Stay", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0a (Armed Stay) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 5, + "propertyName": "Armed Stay", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0a (Armed Stay) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 7, + "propertyName": "Armed Stay", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0a (Armed Stay) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 9, + "propertyName": "Armed Stay", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0a (Armed Stay) - Sound level", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 8, + "propertyName": "Armed Stay", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0a (Armed Stay) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 10, + "propertyKey": 6, + "propertyName": "Armed Stay", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0a (Armed Stay) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 10, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 1, + "propertyName": "Armed Away", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0b (Armed Away) - Multilevel", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 3, + "propertyName": "Armed Away", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0b (Armed Away) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 4, + "propertyName": "Armed Away", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0b (Armed Away) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 5, + "propertyName": "Armed Away", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0b (Armed Away) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 7, + "propertyName": "Armed Away", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0b (Armed Away) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 9, + "propertyName": "Armed Away", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0b (Armed Away) - Sound level", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 8, + "propertyName": "Armed Away", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0b (Armed Away) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 11, + "propertyKey": 6, + "propertyName": "Armed Away", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0b (Armed Away) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 11, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 1, + "propertyName": "Alarming", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0c (Alarming) - Multilevel", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 3, + "propertyName": "Alarming", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0c (Alarming) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 4, + "propertyName": "Alarming", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0c (Alarming) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 5, + "propertyName": "Alarming", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0c (Alarming) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 7, + "propertyName": "Alarming", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0c (Alarming) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 9, + "propertyName": "Alarming", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0c (Alarming) - Sound level", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 8, + "propertyName": "Alarming", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0c (Alarming) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 12, + "propertyKey": 6, + "propertyName": "Alarming", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0c (Alarming) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 12, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 1, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0d (Alarming: Burglar) - Multilevel", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 3, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0d (Alarming: Burglar) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 4, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0d (Alarming: Burglar) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 5, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0d (Alarming: Burglar) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 7, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0d (Alarming: Burglar) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 9, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0d (Alarming: Burglar) - Sound level", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 8, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0d (Alarming: Burglar) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 13, + "propertyKey": 6, + "propertyName": "Alarming: Burglar", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0d (Alarming: Burglar) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 13, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 1, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0e (Alarming: Smoke / Fire) - Multilevel", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 3, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0e (Alarming: Smoke / Fire) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 4, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0e (Alarming: Smoke / Fire) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 5, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0e (Alarming: Smoke / Fire) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 7, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0e (Alarming: Smoke / Fire) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 9, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0e (Alarming: Smoke / Fire) - Sound level", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 8, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0e (Alarming: Smoke / Fire) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 14, + "propertyKey": 6, + "propertyName": "Alarming: Smoke / Fire", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0e (Alarming: Smoke / Fire) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 14, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 1, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x0f (Alarming: Carbon Monoxide) - Multilevel", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 3, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x0f (Alarming: Carbon Monoxide) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 4, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x0f (Alarming: Carbon Monoxide) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 5, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x0f (Alarming: Carbon Monoxide) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 7, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x0f (Alarming: Carbon Monoxide) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 9, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x0f (Alarming: Carbon Monoxide) - Sound level", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 8, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x0f (Alarming: Carbon Monoxide) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 15, + "propertyKey": 6, + "propertyName": "Alarming: Carbon Monoxide", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x0f (Alarming: Carbon Monoxide) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 15, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 1, + "propertyName": "Bypass challenge", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x10 (Bypass challenge) - Multilevel", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 3, + "propertyName": "Bypass challenge", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x10 (Bypass challenge) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 4, + "propertyName": "Bypass challenge", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x10 (Bypass challenge) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 5, + "propertyName": "Bypass challenge", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x10 (Bypass challenge) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 7, + "propertyName": "Bypass challenge", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x10 (Bypass challenge) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 9, + "propertyName": "Bypass challenge", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x10 (Bypass challenge) - Sound level", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 8, + "propertyName": "Bypass challenge", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x10 (Bypass challenge) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 16, + "propertyKey": 6, + "propertyName": "Bypass challenge", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x10 (Bypass challenge) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 16, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 1, + "propertyName": "Entry Delay", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x11 (Entry Delay) - Multilevel", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 3, + "propertyName": "Entry Delay", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x11 (Entry Delay) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 4, + "propertyName": "Entry Delay", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x11 (Entry Delay) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 5, + "propertyName": "Entry Delay", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x11 (Entry Delay) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 7, + "propertyName": "Entry Delay", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x11 (Entry Delay) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 9, + "propertyName": "Entry Delay", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x11 (Entry Delay) - Sound level", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 8, + "propertyName": "Entry Delay", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x11 (Entry Delay) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 17, + "propertyKey": 6, + "propertyName": "Entry Delay", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x11 (Entry Delay) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 17, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 1, + "propertyName": "Exit Delay", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x12 (Exit Delay) - Multilevel", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 3, + "propertyName": "Exit Delay", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x12 (Exit Delay) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 4, + "propertyName": "Exit Delay", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x12 (Exit Delay) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 5, + "propertyName": "Exit Delay", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x12 (Exit Delay) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 7, + "propertyName": "Exit Delay", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x12 (Exit Delay) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 9, + "propertyName": "Exit Delay", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x12 (Exit Delay) - Sound level", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 8, + "propertyName": "Exit Delay", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x12 (Exit Delay) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 18, + "propertyKey": 6, + "propertyName": "Exit Delay", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x12 (Exit Delay) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 18, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 1, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x13 (Alarming: Medical) - Multilevel", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 3, + "propertyName": "Alarming: Medical", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x13 (Alarming: Medical) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 4, + "propertyName": "Alarming: Medical", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x13 (Alarming: Medical) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 5, + "propertyName": "Alarming: Medical", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x13 (Alarming: Medical) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 7, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x13 (Alarming: Medical) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 9, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x13 (Alarming: Medical) - Sound level", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 8, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x13 (Alarming: Medical) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 19, + "propertyKey": 6, + "propertyName": "Alarming: Medical", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x13 (Alarming: Medical) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 19, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 1, + "propertyName": "Node Identify", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x50 (Node Identify) - Multilevel", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 3, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x50 (Node Identify) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 4, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x50 (Node Identify) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 5, + "propertyName": "Node Identify", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x50 (Node Identify) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 7, + "propertyName": "Node Identify", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x50 (Node Identify) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 9, + "propertyName": "Node Identify", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x50 (Node Identify) - Sound level", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 8, + "propertyName": "Node Identify", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x50 (Node Identify) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 80, + "propertyKey": 6, + "propertyName": "Node Identify", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x50 (Node Identify) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 80, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 1, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x60 (Generic event sound notification 1) - Multilevel", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 3, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x60 (Generic event sound notification 1) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 4, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x60 (Generic event sound notification 1) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 5, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x60 (Generic event sound notification 1) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 7, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x60 (Generic event sound notification 1) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 9, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x60 (Generic event sound notification 1) - Sound level", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 8, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x60 (Generic event sound notification 1) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 96, + "propertyKey": 6, + "propertyName": "Generic event sound notification 1", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x60 (Generic event sound notification 1) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 96, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 1, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x61 (Generic event sound notification 2) - Multilevel", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 3, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x61 (Generic event sound notification 2) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 4, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x61 (Generic event sound notification 2) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 5, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x61 (Generic event sound notification 2) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 7, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x61 (Generic event sound notification 2) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 9, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x61 (Generic event sound notification 2) - Sound level", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 8, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x61 (Generic event sound notification 2) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 97, + "propertyKey": 6, + "propertyName": "Generic event sound notification 2", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x61 (Generic event sound notification 2) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 97, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 1, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x62 (Generic event sound notification 3) - Multilevel", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 3, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x62 (Generic event sound notification 3) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 4, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x62 (Generic event sound notification 3) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 5, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x62 (Generic event sound notification 3) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 7, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x62 (Generic event sound notification 3) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 9, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x62 (Generic event sound notification 3) - Sound level", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 8, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x62 (Generic event sound notification 3) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 98, + "propertyKey": 6, + "propertyName": "Generic event sound notification 3", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x62 (Generic event sound notification 3) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 98, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 1, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x63 (Generic event sound notification 4) - Multilevel", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 3, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x63 (Generic event sound notification 4) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 4, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x63 (Generic event sound notification 4) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 5, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x63 (Generic event sound notification 4) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 7, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x63 (Generic event sound notification 4) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 9, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x63 (Generic event sound notification 4) - Sound level", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 8, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x63 (Generic event sound notification 4) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 99, + "propertyKey": 6, + "propertyName": "Generic event sound notification 4", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x63 (Generic event sound notification 4) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 99, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 1, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x64 (Generic event sound notification 5) - Multilevel", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 3, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x64 (Generic event sound notification 5) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 4, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x64 (Generic event sound notification 5) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 5, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x64 (Generic event sound notification 5) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 7, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x64 (Generic event sound notification 5) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 9, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x64 (Generic event sound notification 5) - Sound level", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 8, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x64 (Generic event sound notification 5) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 100, + "propertyKey": 6, + "propertyName": "Generic event sound notification 5", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x64 (Generic event sound notification 5) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 100, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 1, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x14 (Alarming: Freeze warning) - Multilevel", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 3, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x14 (Alarming: Freeze warning) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 4, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x14 (Alarming: Freeze warning) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 5, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x14 (Alarming: Freeze warning) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 7, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x14 (Alarming: Freeze warning) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 9, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x14 (Alarming: Freeze warning) - Sound level", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 8, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x14 (Alarming: Freeze warning) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 20, + "propertyKey": 6, + "propertyName": "Alarming: Freeze warning", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x14 (Alarming: Freeze warning) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 20, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 1, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x15 (Alarming: Water leak) - Multilevel", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 3, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x15 (Alarming: Water leak) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 4, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x15 (Alarming: Water leak) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 5, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x15 (Alarming: Water leak) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 7, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x15 (Alarming: Water leak) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 9, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x15 (Alarming: Water leak) - Sound level", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 8, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x15 (Alarming: Water leak) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 21, + "propertyKey": 6, + "propertyName": "Alarming: Water leak", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x15 (Alarming: Water leak) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 21, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 1, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x81 (Manufacturer defined 2) - Multilevel", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 3, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x81 (Manufacturer defined 2) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 4, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x81 (Manufacturer defined 2) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 5, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x81 (Manufacturer defined 2) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 7, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x81 (Manufacturer defined 2) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 9, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x81 (Manufacturer defined 2) - Sound level", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 8, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x81 (Manufacturer defined 2) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 129, + "propertyKey": 6, + "propertyName": "Manufacturer defined 2", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x81 (Manufacturer defined 2) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 129, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 1, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x82 (Manufacturer defined 3) - Multilevel", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 3, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x82 (Manufacturer defined 3) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 4, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x82 (Manufacturer defined 3) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 5, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x82 (Manufacturer defined 3) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 7, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x82 (Manufacturer defined 3) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 9, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x82 (Manufacturer defined 3) - Sound level", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 8, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x82 (Manufacturer defined 3) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 130, + "propertyKey": 6, + "propertyName": "Manufacturer defined 3", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x82 (Manufacturer defined 3) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 130, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 1, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x83 (Manufacturer defined 4) - Multilevel", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 3, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x83 (Manufacturer defined 4) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 4, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x83 (Manufacturer defined 4) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 5, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x83 (Manufacturer defined 4) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 7, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x83 (Manufacturer defined 4) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 9, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x83 (Manufacturer defined 4) - Sound level", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 8, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x83 (Manufacturer defined 4) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 131, + "propertyKey": 6, + "propertyName": "Manufacturer defined 4", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x83 (Manufacturer defined 4) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 131, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 1, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x84 (Manufacturer defined 5) - Multilevel", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 3, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x84 (Manufacturer defined 5) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 4, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x84 (Manufacturer defined 5) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 5, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x84 (Manufacturer defined 5) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 7, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x84 (Manufacturer defined 5) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 9, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x84 (Manufacturer defined 5) - Sound level", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 8, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x84 (Manufacturer defined 5) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 132, + "propertyKey": 6, + "propertyName": "Manufacturer defined 5", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x84 (Manufacturer defined 5) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 132, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 1, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Multilevel", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "0x85 (Manufacturer defined 6) - Multilevel", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 1 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 3, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "On/Off Period: Duration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the duration of an on/off period in 1/10th seconds. Must be set together with \"On/Off Cycle Count\"", + "label": "0x85 (Manufacturer defined 6) - On/Off Period: Duration", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 3 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 4, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "On/Off Cycle Count", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Sets the number of on/off periods. 0xff means infinite. Must be set together with \"On/Off Period duration\"", + "label": "0x85 (Manufacturer defined 6) - On/Off Cycle Count", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 4 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 5, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "On/Off Period: On time", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the length of the On time during an On/Off period. It allows asymmetric On/Off periods. The value 0x00 MUST represent symmetric On/Off period (On time equal to Off time)", + "label": "0x85 (Manufacturer defined 6) - On/Off Period: On time", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 5 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 7, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Timeout: Seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of seconds. Can be used together with other timeout properties", + "label": "0x85 (Manufacturer defined 6) - Timeout: Seconds", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 7 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 9, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Sound level", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "This property is used to set the volume of a indicator. 0 means off/mute.", + "label": "0x85 (Manufacturer defined 6) - Sound level", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 9 + }, + "max": 100, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 8, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Timeout: 1/100th seconds", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of 1/100th seconds. Can be used together with other timeout properties", + "label": "0x85 (Manufacturer defined 6) - Timeout: 1/100th seconds", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 8 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": 133, + "propertyKey": 6, + "propertyName": "Manufacturer defined 6", + "propertyKeyName": "Timeout: Minutes", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Turns the indicator of after this amount of minutes. Can be used together with other timeout properties", + "label": "0x85 (Manufacturer defined 6) - Timeout: Minutes", + "ccSpecific": { + "indicatorId": 133, + "propertyId": 6 + }, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "value", + "propertyName": "value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Indicator value", + "ccSpecific": { + "indicatorId": 0 + }, + "min": 0, + "max": 255, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 135, + "commandClassName": "Indicator", + "property": "identify", + "propertyName": "identify", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Identify", + "states": { + "true": "Identify" + }, + "stateful": true, + "secret": false + } + } + ], + "endpoints": [ + { + "nodeId": 4, + "index": 0, + "installerIcon": 8193, + "userIcon": 8193, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 11, + "label": "Secure Keypad" + } + }, + "commandClasses": [ + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 2, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 111, + "name": "Entry Control", + "version": 1, + "isSecure": true + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index 93ac52f9041..5dfbb0f5bd8 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -1,10 +1,13 @@ """Test the Z-Wave JS binary sensor platform.""" +from datetime import timedelta + import pytest from zwave_js_server.event import Event from zwave_js_server.model.node import Node from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_OFF, @@ -15,17 +18,17 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .common import ( DISABLED_LEGACY_BINARY_SENSOR, ENABLED_LEGACY_BINARY_SENSOR, - LOW_BATTERY_BINARY_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR, PROPERTY_DOOR_STATUS_BINARY_SENSOR, TAMPER_SENSOR, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture @@ -34,21 +37,56 @@ def platforms() -> list[str]: return [Platform.BINARY_SENSOR] -async def test_low_battery_sensor( - hass: HomeAssistant, entity_registry: er.EntityRegistry, multisensor_6, integration +async def test_battery_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ring_keypad: Node, + integration: MockConfigEntry, ) -> None: - """Test boolean binary sensor of type low battery.""" - state = hass.states.get(LOW_BATTERY_BINARY_SENSOR) + """Test boolean battery binary sensors.""" + entity_id = "binary_sensor.keypad_v2_low_battery_level" + state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.BATTERY - entity_entry = entity_registry.async_get(LOW_BATTERY_BINARY_SENSOR) + entity_entry = entity_registry.async_get(entity_id) assert entity_entry assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + disabled_binary_sensor_battery_entities = ( + "binary_sensor.keypad_v2_battery_is_disconnected", + "binary_sensor.keypad_v2_fluid_is_low", + "binary_sensor.keypad_v2_overheating", + "binary_sensor.keypad_v2_rechargeable", + "binary_sensor.keypad_v2_used_as_backup", + ) + + for entity_id in disabled_binary_sensor_battery_entities: + state = hass.states.get(entity_id) + assert state is None # disabled by default + + entity_entry = entity_registry.async_get(entity_id) + + assert entity_entry + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + for entity_id in disabled_binary_sensor_battery_entities: + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + async def test_enabled_legacy_sensor( hass: HomeAssistant, ecolink_door_sensor, integration diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index c93b722334b..c3580df1f27 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,6 +1,7 @@ """Test the Z-Wave JS sensor platform.""" import copy +from datetime import timedelta import pytest from zwave_js_server.const.command_class.meter import MeterType @@ -26,6 +27,7 @@ from homeassistant.components.zwave_js.sensor import ( CONTROLLER_STATISTICS_KEY_MAP, NODE_STATISTICS_KEY_MAP, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -35,6 +37,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UV_INDEX, EntityCategory, + Platform, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -45,6 +48,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .common import ( AIR_TEMPERATURE_SENSOR, @@ -57,7 +61,94 @@ from .common import ( VOLTAGE_SENSOR, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.SENSOR] + + +async def test_battery_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + ring_keypad: Node, + integration: MockConfigEntry, +) -> None: + """Test numeric battery sensors.""" + entity_id = "sensor.keypad_v2_battery_level" + state = hass.states.get(entity_id) + assert state + assert state.state == "100.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + + disabled_sensor_battery_entities = ( + "sensor.keypad_v2_chargingstatus", + "sensor.keypad_v2_maximum_capacity", + "sensor.keypad_v2_rechargeorreplace", + "sensor.keypad_v2_temperature", + ) + + for entity_id in disabled_sensor_battery_entities: + state = hass.states.get(entity_id) + assert state is None # disabled by default + + entity_entry = entity_registry.async_get(entity_id) + + assert entity_entry + assert entity_entry.entity_category is EntityCategory.DIAGNOSTIC + assert entity_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + entity_registry.async_update_entity(entity_id, disabled_by=None) + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + entity_id = "sensor.keypad_v2_chargingstatus" + state = hass.states.get(entity_id) + assert state + assert state.state == "Maintaining" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM + assert ATTR_STATE_CLASS not in state.attributes + + entity_id = "sensor.keypad_v2_maximum_capacity" + state = hass.states.get(entity_id) + assert state + assert ( + state.state == "0" + ) # This should be None/unknown but will be fixed in a future PR. + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + entity_id = "sensor.keypad_v2_rechargeorreplace" + state = hass.states.get(entity_id) + assert state + assert state.state == "No" + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM + assert ATTR_STATE_CLASS not in state.attributes + + entity_id = "sensor.keypad_v2_temperature" + state = hass.states.get(entity_id) + assert state + assert ( + state.state == "0" + ) # This should be None/unknown but will be fixed in a future PR. + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT async def test_numeric_sensor( From 4aff0324428512a6e868e24eeb7c47cbe8677aab Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:55:14 +0200 Subject: [PATCH 19/69] Bump homematicip to 2.0.6 (#147151) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 163f3c402dc..d5af2859873 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", "loggers": ["homematicip"], - "requirements": ["homematicip==2.0.5"] + "requirements": ["homematicip==2.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index fa23a9566fc..a3f0c833d2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud -homematicip==2.0.5 +homematicip==2.0.6 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7188e1f1395..a27b9f5d199 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.3 home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud -homematicip==2.0.5 +homematicip==2.0.6 # homeassistant.components.remember_the_milk httplib2==0.20.4 From b00342991272df2c0958fdf370572eef35c2ca20 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:04:28 -0700 Subject: [PATCH 20/69] Expose statistics selector, use for `recorder.get_statistics` (#147056) * Expose statistics selector, use for `recorder.get_statistics` * code review * syntax formatting * rerun ci --- .../components/recorder/services.yaml | 2 +- homeassistant/helpers/selector.py | 33 +++++++++++++++++++ tests/helpers/test_selector.py | 27 +++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 65aa797d91b..3ecd2be8af6 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -69,7 +69,7 @@ get_statistics: - sensor.energy_consumption - sensor.temperature selector: - entity: + statistic: multiple: true period: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 2d7fd51cac7..322cfe34042 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1216,6 +1216,39 @@ class SelectSelector(Selector[SelectSelectorConfig]): return [parent_schema(vol.Schema(str)(val)) for val in data] +class StatisticSelectorConfig(BaseSelectorConfig, total=False): + """Class to represent a statistic selector config.""" + + multiple: bool + + +@SELECTORS.register("statistic") +class StatisticSelector(Selector[StatisticSelectorConfig]): + """Selector of a single or list of statistics.""" + + selector_type = "statistic" + + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + { + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __init__(self, config: StatisticSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + + if not self.config["multiple"]: + stat: str = vol.Schema(str)(data) + return stat + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + class TargetSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a target selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 3ddbecaf48d..51ee467b029 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1262,3 +1262,30 @@ def test_label_selector_schema(schema, valid_selections, invalid_selections) -> def test_floor_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test floor selector.""" _test_selector("floor", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + [ + ( + {}, + ("sensor.temperature",), + (None, ["sensor.temperature"]), + ), + ( + {"multiple": True}, + (["sensor.temperature", "sensor:external_temperature"], []), + ("sensor.temperature",), + ), + ( + {"multiple": False}, + ("sensor.temperature",), + (None, ["sensor.temperature"]), + ), + ], +) +def test_statistic_selector_schema( + schema, valid_selections, invalid_selections +) -> None: + """Test statistic selector.""" + _test_selector("statistic", schema, valid_selections, invalid_selections) From cf67a6845485880912fbca5e3e50a9fae6879200 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:24:51 +0200 Subject: [PATCH 21/69] Use PEP 695 TypeVar syntax for paperless_ngx (#147156) --- .../components/paperless_ngx/coordinator.py | 9 +++------ .../components/paperless_ngx/entity.py | 10 ++++------ .../components/paperless_ngx/sensor.py | 17 +++++++++-------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/paperless_ngx/coordinator.py b/homeassistant/components/paperless_ngx/coordinator.py index d5960bed49b..270fd8063dc 100644 --- a/homeassistant/components/paperless_ngx/coordinator.py +++ b/homeassistant/components/paperless_ngx/coordinator.py @@ -5,7 +5,6 @@ from __future__ import annotations from abc import abstractmethod from dataclasses import dataclass from datetime import timedelta -from typing import TypeVar from pypaperless import Paperless from pypaperless.exceptions import ( @@ -25,8 +24,6 @@ from .const import DOMAIN, LOGGER type PaperlessConfigEntry = ConfigEntry[PaperlessData] -TData = TypeVar("TData") - UPDATE_INTERVAL_STATISTICS = timedelta(seconds=120) UPDATE_INTERVAL_STATUS = timedelta(seconds=300) @@ -39,7 +36,7 @@ class PaperlessData: status: PaperlessStatusCoordinator -class PaperlessCoordinator(DataUpdateCoordinator[TData]): +class PaperlessCoordinator[DataT](DataUpdateCoordinator[DataT]): """Coordinator to manage fetching Paperless-ngx API.""" config_entry: PaperlessConfigEntry @@ -63,7 +60,7 @@ class PaperlessCoordinator(DataUpdateCoordinator[TData]): update_interval=update_interval, ) - async def _async_update_data(self) -> TData: + async def _async_update_data(self) -> DataT: """Update data via internal method.""" try: return await self._async_update_data_internal() @@ -89,7 +86,7 @@ class PaperlessCoordinator(DataUpdateCoordinator[TData]): ) from err @abstractmethod - async def _async_update_data_internal(self) -> TData: + async def _async_update_data_internal(self) -> DataT: """Update data via paperless-ngx API.""" diff --git a/homeassistant/components/paperless_ngx/entity.py b/homeassistant/components/paperless_ngx/entity.py index e7eb0f0edcf..59cd13c5209 100644 --- a/homeassistant/components/paperless_ngx/entity.py +++ b/homeassistant/components/paperless_ngx/entity.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Generic, TypeVar - from homeassistant.components.sensor import EntityDescription from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,17 +9,17 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import PaperlessCoordinator -TCoordinator = TypeVar("TCoordinator", bound=PaperlessCoordinator) - -class PaperlessEntity(CoordinatorEntity[TCoordinator], Generic[TCoordinator]): +class PaperlessEntity[CoordinatorT: PaperlessCoordinator]( + CoordinatorEntity[CoordinatorT] +): """Defines a base Paperless-ngx entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: TCoordinator, + coordinator: CoordinatorT, description: EntityDescription, ) -> None: """Initialize the Paperless-ngx entity.""" diff --git a/homeassistant/components/paperless_ngx/sensor.py b/homeassistant/components/paperless_ngx/sensor.py index e3f601b68e6..5d6bfe1347e 100644 --- a/homeassistant/components/paperless_ngx/sensor.py +++ b/homeassistant/components/paperless_ngx/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic from pypaperless.models import Statistic, Status from pypaperless.models.common import StatusType @@ -23,23 +22,23 @@ from homeassistant.util.unit_conversion import InformationConverter from .coordinator import ( PaperlessConfigEntry, + PaperlessCoordinator, PaperlessStatisticCoordinator, PaperlessStatusCoordinator, - TData, ) -from .entity import PaperlessEntity, TCoordinator +from .entity import PaperlessEntity PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) -class PaperlessEntityDescription(SensorEntityDescription, Generic[TData]): +class PaperlessEntityDescription[DataT](SensorEntityDescription): """Describes Paperless-ngx sensor entity.""" - value_fn: Callable[[TData], StateType] + value_fn: Callable[[DataT], StateType] -SENSOR_STATISTICS: tuple[PaperlessEntityDescription, ...] = ( +SENSOR_STATISTICS: tuple[PaperlessEntityDescription[Statistic], ...] = ( PaperlessEntityDescription[Statistic]( key="documents_total", translation_key="documents_total", @@ -78,7 +77,7 @@ SENSOR_STATISTICS: tuple[PaperlessEntityDescription, ...] = ( ), ) -SENSOR_STATUS: tuple[PaperlessEntityDescription, ...] = ( +SENSOR_STATUS: tuple[PaperlessEntityDescription[Status], ...] = ( PaperlessEntityDescription[Status]( key="storage_total", translation_key="storage_total", @@ -258,7 +257,9 @@ async def async_setup_entry( async_add_entities(entities) -class PaperlessSensor(PaperlessEntity[TCoordinator], SensorEntity): +class PaperlessSensor[CoordinatorT: PaperlessCoordinator]( + PaperlessEntity[CoordinatorT], SensorEntity +): """Defines a Paperless-ngx sensor entity.""" entity_description: PaperlessEntityDescription From b8dfb2c85001ac3f050914f5ce206a32d5995bd2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:25:45 +0200 Subject: [PATCH 22/69] Use PEP 695 TypeVar syntax for eheimdigital (#147154) --- .../components/eheimdigital/number.py | 26 +++++++++---------- .../components/eheimdigital/select.py | 24 ++++++++--------- .../components/eheimdigital/sensor.py | 22 ++++++++-------- homeassistant/components/eheimdigital/time.py | 22 +++++++--------- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/eheimdigital/number.py b/homeassistant/components/eheimdigital/number.py index 03f27aa82df..53382e3aead 100644 --- a/homeassistant/components/eheimdigital/number.py +++ b/homeassistant/components/eheimdigital/number.py @@ -2,7 +2,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Generic, TypeVar, override +from typing import Any, override from eheimdigital.classic_vario import EheimDigitalClassicVario from eheimdigital.device import EheimDigitalDevice @@ -30,16 +30,16 @@ from .entity import EheimDigitalEntity, exception_handler PARALLEL_UPDATES = 0 -_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True) - @dataclass(frozen=True, kw_only=True) -class EheimDigitalNumberDescription(NumberEntityDescription, Generic[_DeviceT_co]): +class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice]( + NumberEntityDescription +): """Class describing EHEIM Digital sensor entities.""" - value_fn: Callable[[_DeviceT_co], float | None] - set_value_fn: Callable[[_DeviceT_co, float], Awaitable[None]] - uom_fn: Callable[[_DeviceT_co], str] | None = None + value_fn: Callable[[_DeviceT], float | None] + set_value_fn: Callable[[_DeviceT, float], Awaitable[None]] + uom_fn: Callable[[_DeviceT], str] | None = None CLASSICVARIO_DESCRIPTIONS: tuple[ @@ -136,7 +136,7 @@ async def async_setup_entry( device_address: dict[str, EheimDigitalDevice], ) -> None: """Set up the number entities for one or multiple devices.""" - entities: list[EheimDigitalNumber[EheimDigitalDevice]] = [] + entities: list[EheimDigitalNumber[Any]] = [] for device in device_address.values(): if isinstance(device, EheimDigitalClassicVario): entities.extend( @@ -163,18 +163,18 @@ async def async_setup_entry( async_setup_device_entities(coordinator.hub.devices) -class EheimDigitalNumber( - EheimDigitalEntity[_DeviceT_co], NumberEntity, Generic[_DeviceT_co] +class EheimDigitalNumber[_DeviceT: EheimDigitalDevice]( + EheimDigitalEntity[_DeviceT], NumberEntity ): """Represent a EHEIM Digital number entity.""" - entity_description: EheimDigitalNumberDescription[_DeviceT_co] + entity_description: EheimDigitalNumberDescription[_DeviceT] def __init__( self, coordinator: EheimDigitalUpdateCoordinator, - device: _DeviceT_co, - description: EheimDigitalNumberDescription[_DeviceT_co], + device: _DeviceT, + description: EheimDigitalNumberDescription[_DeviceT], ) -> None: """Initialize an EHEIM Digital number entity.""" super().__init__(coordinator, device) diff --git a/homeassistant/components/eheimdigital/select.py b/homeassistant/components/eheimdigital/select.py index 41ab13e3bd4..5c42055441a 100644 --- a/homeassistant/components/eheimdigital/select.py +++ b/homeassistant/components/eheimdigital/select.py @@ -2,7 +2,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Generic, TypeVar, override +from typing import Any, override from eheimdigital.classic_vario import EheimDigitalClassicVario from eheimdigital.device import EheimDigitalDevice @@ -17,15 +17,15 @@ from .entity import EheimDigitalEntity, exception_handler PARALLEL_UPDATES = 0 -_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True) - @dataclass(frozen=True, kw_only=True) -class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]): +class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice]( + SelectEntityDescription +): """Class describing EHEIM Digital select entities.""" - value_fn: Callable[[_DeviceT_co], str | None] - set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]] + value_fn: Callable[[_DeviceT], str | None] + set_value_fn: Callable[[_DeviceT, str], Awaitable[None]] CLASSICVARIO_DESCRIPTIONS: tuple[ @@ -59,7 +59,7 @@ async def async_setup_entry( device_address: dict[str, EheimDigitalDevice], ) -> None: """Set up the number entities for one or multiple devices.""" - entities: list[EheimDigitalSelect[EheimDigitalDevice]] = [] + entities: list[EheimDigitalSelect[Any]] = [] for device in device_address.values(): if isinstance(device, EheimDigitalClassicVario): entities.extend( @@ -75,18 +75,18 @@ async def async_setup_entry( async_setup_device_entities(coordinator.hub.devices) -class EheimDigitalSelect( - EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co] +class EheimDigitalSelect[_DeviceT: EheimDigitalDevice]( + EheimDigitalEntity[_DeviceT], SelectEntity ): """Represent an EHEIM Digital select entity.""" - entity_description: EheimDigitalSelectDescription[_DeviceT_co] + entity_description: EheimDigitalSelectDescription[_DeviceT] def __init__( self, coordinator: EheimDigitalUpdateCoordinator, - device: _DeviceT_co, - description: EheimDigitalSelectDescription[_DeviceT_co], + device: _DeviceT, + description: EheimDigitalSelectDescription[_DeviceT], ) -> None: """Initialize an EHEIM Digital select entity.""" super().__init__(coordinator, device) diff --git a/homeassistant/components/eheimdigital/sensor.py b/homeassistant/components/eheimdigital/sensor.py index 3d809cc14dc..82038b40865 100644 --- a/homeassistant/components/eheimdigital/sensor.py +++ b/homeassistant/components/eheimdigital/sensor.py @@ -2,7 +2,7 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Generic, TypeVar, override +from typing import Any, override from eheimdigital.classic_vario import EheimDigitalClassicVario from eheimdigital.device import EheimDigitalDevice @@ -20,14 +20,14 @@ from .entity import EheimDigitalEntity # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 -_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True) - @dataclass(frozen=True, kw_only=True) -class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]): +class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice]( + SensorEntityDescription +): """Class describing EHEIM Digital sensor entities.""" - value_fn: Callable[[_DeviceT_co], float | str | None] + value_fn: Callable[[_DeviceT], float | str | None] CLASSICVARIO_DESCRIPTIONS: tuple[ @@ -75,7 +75,7 @@ async def async_setup_entry( device_address: dict[str, EheimDigitalDevice], ) -> None: """Set up the light entities for one or multiple devices.""" - entities: list[EheimDigitalSensor[EheimDigitalDevice]] = [] + entities: list[EheimDigitalSensor[Any]] = [] for device in device_address.values(): if isinstance(device, EheimDigitalClassicVario): entities += [ @@ -91,18 +91,18 @@ async def async_setup_entry( async_setup_device_entities(coordinator.hub.devices) -class EheimDigitalSensor( - EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co] +class EheimDigitalSensor[_DeviceT: EheimDigitalDevice]( + EheimDigitalEntity[_DeviceT], SensorEntity ): """Represent a EHEIM Digital sensor entity.""" - entity_description: EheimDigitalSensorDescription[_DeviceT_co] + entity_description: EheimDigitalSensorDescription[_DeviceT] def __init__( self, coordinator: EheimDigitalUpdateCoordinator, - device: _DeviceT_co, - description: EheimDigitalSensorDescription[_DeviceT_co], + device: _DeviceT, + description: EheimDigitalSensorDescription[_DeviceT], ) -> None: """Initialize an EHEIM Digital number entity.""" super().__init__(coordinator, device) diff --git a/homeassistant/components/eheimdigital/time.py b/homeassistant/components/eheimdigital/time.py index 49834c827b9..f14a4150eff 100644 --- a/homeassistant/components/eheimdigital/time.py +++ b/homeassistant/components/eheimdigital/time.py @@ -3,7 +3,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import time -from typing import Generic, TypeVar, final, override +from typing import Any, final, override from eheimdigital.classic_vario import EheimDigitalClassicVario from eheimdigital.device import EheimDigitalDevice @@ -19,15 +19,13 @@ from .entity import EheimDigitalEntity, exception_handler PARALLEL_UPDATES = 0 -_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True) - @dataclass(frozen=True, kw_only=True) -class EheimDigitalTimeDescription(TimeEntityDescription, Generic[_DeviceT_co]): +class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescription): """Class describing EHEIM Digital time entities.""" - value_fn: Callable[[_DeviceT_co], time | None] - set_value_fn: Callable[[_DeviceT_co, time], Awaitable[None]] + value_fn: Callable[[_DeviceT], time | None] + set_value_fn: Callable[[_DeviceT, time], Awaitable[None]] CLASSICVARIO_DESCRIPTIONS: tuple[ @@ -79,7 +77,7 @@ async def async_setup_entry( device_address: dict[str, EheimDigitalDevice], ) -> None: """Set up the time entities for one or multiple devices.""" - entities: list[EheimDigitalTime[EheimDigitalDevice]] = [] + entities: list[EheimDigitalTime[Any]] = [] for device in device_address.values(): if isinstance(device, EheimDigitalClassicVario): entities.extend( @@ -103,18 +101,18 @@ async def async_setup_entry( @final -class EheimDigitalTime( - EheimDigitalEntity[_DeviceT_co], TimeEntity, Generic[_DeviceT_co] +class EheimDigitalTime[_DeviceT: EheimDigitalDevice]( + EheimDigitalEntity[_DeviceT], TimeEntity ): """Represent an EHEIM Digital time entity.""" - entity_description: EheimDigitalTimeDescription[_DeviceT_co] + entity_description: EheimDigitalTimeDescription[_DeviceT] def __init__( self, coordinator: EheimDigitalUpdateCoordinator, - device: _DeviceT_co, - description: EheimDigitalTimeDescription[_DeviceT_co], + device: _DeviceT, + description: EheimDigitalTimeDescription[_DeviceT], ) -> None: """Initialize an EHEIM Digital time entity.""" super().__init__(coordinator, device) From 73d0d87705773fabfd37edf7bea5ea12bb1e75d3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:26:07 +0200 Subject: [PATCH 23/69] Use PEP 695 TypeVar syntax for nextdns (#147155) --- homeassistant/components/nextdns/coordinator.py | 8 ++++---- homeassistant/components/nextdns/entity.py | 8 ++++++-- homeassistant/components/nextdns/sensor.py | 13 +++++++------ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nextdns/coordinator.py b/homeassistant/components/nextdns/coordinator.py index 3bc5dfe60d1..9b82e82ffe0 100644 --- a/homeassistant/components/nextdns/coordinator.py +++ b/homeassistant/components/nextdns/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING from aiohttp.client_exceptions import ClientConnectorError from nextdns import ( @@ -33,10 +33,10 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData) - -class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]): +class NextDnsUpdateCoordinator[CoordinatorDataT: NextDnsData]( + DataUpdateCoordinator[CoordinatorDataT] +): """Class to manage fetching NextDNS data API.""" config_entry: NextDnsConfigEntry diff --git a/homeassistant/components/nextdns/entity.py b/homeassistant/components/nextdns/entity.py index 26e0a5dd9ef..7e86d1d246c 100644 --- a/homeassistant/components/nextdns/entity.py +++ b/homeassistant/components/nextdns/entity.py @@ -1,14 +1,18 @@ """Define NextDNS entities.""" +from nextdns.model import NextDnsData + from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator +from .coordinator import NextDnsUpdateCoordinator -class NextDnsEntity(CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]]): +class NextDnsEntity[CoordinatorDataT: NextDnsData]( + CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]] +): """Define NextDNS entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index b03f262cbeb..1b43f7c9c25 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic from nextdns import ( AnalyticsDnssec, @@ -13,6 +12,7 @@ from nextdns import ( AnalyticsProtocols, AnalyticsStatus, ) +from nextdns.model import NextDnsData from homeassistant.components.sensor import ( SensorEntity, @@ -32,15 +32,14 @@ from .const import ( ATTR_PROTOCOLS, ATTR_STATUS, ) -from .coordinator import CoordinatorDataT from .entity import NextDnsEntity PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) -class NextDnsSensorEntityDescription( - SensorEntityDescription, Generic[CoordinatorDataT] +class NextDnsSensorEntityDescription[CoordinatorDataT: NextDnsData]( + SensorEntityDescription ): """NextDNS sensor entity description.""" @@ -297,10 +296,12 @@ async def async_setup_entry( ) -class NextDnsSensor(NextDnsEntity, SensorEntity): +class NextDnsSensor[CoordinatorDataT: NextDnsData]( + NextDnsEntity[CoordinatorDataT], SensorEntity +): """Define an NextDNS sensor.""" - entity_description: NextDnsSensorEntityDescription + entity_description: NextDnsSensorEntityDescription[CoordinatorDataT] @property def native_value(self) -> StateType: From 2c13c70e128a5efdcad2ec76970f90236fdeb8f0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:39:09 +0200 Subject: [PATCH 24/69] Update ruff to 0.12.0 (#147106) --- .pre-commit-config.yaml | 2 +- homeassistant/__main__.py | 12 +-- homeassistant/auth/mfa_modules/notify.py | 8 +- homeassistant/auth/mfa_modules/totp.py | 10 +-- homeassistant/bootstrap.py | 9 +-- homeassistant/components/airly/config_flow.py | 6 +- .../components/automation/helpers.py | 5 +- homeassistant/components/backup/__init__.py | 6 +- homeassistant/components/control4/__init__.py | 4 +- .../components/conversation/__init__.py | 2 +- .../components/downloader/services.py | 3 +- homeassistant/components/esphome/dashboard.py | 4 +- homeassistant/components/frontend/__init__.py | 3 +- .../components/google_assistant/helpers.py | 11 ++- .../components/hassio/update_helper.py | 9 +-- .../silabs_multiprotocol_addon.py | 21 ++---- .../homekit_controller/config_flow.py | 9 +-- homeassistant/components/http/__init__.py | 3 +- homeassistant/components/http/ban.py | 3 +- homeassistant/components/intent/timers.py | 5 +- homeassistant/components/mqtt/__init__.py | 6 +- homeassistant/components/mqtt/client.py | 22 ++---- homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/entity.py | 3 +- homeassistant/components/mqtt/util.py | 6 +- homeassistant/components/network/__init__.py | 4 +- homeassistant/components/notify/legacy.py | 3 +- .../components/ollama/conversation.py | 5 +- homeassistant/components/onboarding/views.py | 6 +- homeassistant/components/profiler/__init__.py | 18 ++--- .../recorder/auto_repairs/schema.py | 6 +- .../components/recorder/history/__init__.py | 10 +-- homeassistant/components/recorder/pool.py | 4 +- .../components/recorder/statistics.py | 2 +- homeassistant/components/recorder/util.py | 7 +- .../components/rmvtransport/sensor.py | 3 +- homeassistant/components/script/helpers.py | 2 +- homeassistant/components/ssdp/server.py | 2 +- homeassistant/components/stream/__init__.py | 13 ++-- homeassistant/components/stream/core.py | 8 +- homeassistant/components/stream/fmp4utils.py | 4 +- .../components/system_health/__init__.py | 2 +- homeassistant/components/template/helpers.py | 3 +- homeassistant/components/template/light.py | 1 - .../components/tensorflow/image_processing.py | 9 +-- .../components/thread/diagnostics.py | 4 +- homeassistant/components/tplink/sensor.py | 3 +- homeassistant/components/tts/media_source.py | 4 +- homeassistant/components/tuya/__init__.py | 2 +- .../components/websocket_api/commands.py | 15 ++-- homeassistant/components/zha/websocket_api.py | 4 +- .../components/zwave_js/triggers/event.py | 4 +- homeassistant/config.py | 3 +- homeassistant/core.py | 25 +++---- homeassistant/core_config.py | 11 ++- homeassistant/exceptions.py | 3 +- homeassistant/helpers/area_registry.py | 9 +-- homeassistant/helpers/backup.py | 3 +- homeassistant/helpers/config_entry_flow.py | 9 +-- homeassistant/helpers/config_validation.py | 13 ++-- homeassistant/helpers/deprecation.py | 7 +- homeassistant/helpers/device_registry.py | 6 +- homeassistant/helpers/entity_registry.py | 3 +- homeassistant/helpers/json.py | 5 +- homeassistant/helpers/llm.py | 3 +- homeassistant/helpers/network.py | 6 +- homeassistant/helpers/recorder.py | 11 ++- homeassistant/helpers/service.py | 6 +- homeassistant/helpers/storage.py | 2 +- homeassistant/helpers/sun.py | 4 +- homeassistant/helpers/system_info.py | 3 +- homeassistant/helpers/template.py | 25 +++---- homeassistant/helpers/typing.py | 3 +- homeassistant/loader.py | 6 +- homeassistant/scripts/check_config.py | 3 +- homeassistant/setup.py | 3 +- homeassistant/util/async_.py | 3 +- homeassistant/util/signal_type.pyi | 5 +- pyproject.toml | 5 ++ requirements_test_pre_commit.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- script/lint_and_test.py | 3 +- script/version_bump.py | 2 +- tests/common.py | 9 +-- tests/components/backup/conftest.py | 3 +- tests/components/conftest.py | 75 +++++++------------ .../dlna_dms/test_dms_device_source.py | 6 +- tests/components/keyboard/test_init.py | 4 +- tests/components/lirc/test_init.py | 4 +- tests/components/mqtt/test_init.py | 13 ++-- tests/components/nibe_heatpump/conftest.py | 3 +- tests/components/sms/test_init.py | 2 +- tests/conftest.py | 59 +++++---------- tests/helpers/test_frame.py | 10 ++- tests/test_loader.py | 10 +-- tests/test_main.py | 4 +- 96 files changed, 291 insertions(+), 427 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf896f8b12c..30351a9381e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.12 + rev: v0.12.0 hooks: - id: ruff-check args: diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b9d98832705..6fd48c4809c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -38,8 +38,7 @@ def validate_python() -> None: def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" - # pylint: disable-next=import-outside-toplevel - from . import config as config_util + from . import config as config_util # noqa: PLC0415 lib_dir = os.path.join(config_dir, "deps") @@ -80,8 +79,7 @@ def ensure_config_path(config_dir: str) -> None: def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" - # pylint: disable-next=import-outside-toplevel - from . import config as config_util + from . import config as config_util # noqa: PLC0415 parser = argparse.ArgumentParser( description="Home Assistant: Observe, Control, Automate.", @@ -177,8 +175,7 @@ def main() -> int: validate_os() if args.script is not None: - # pylint: disable-next=import-outside-toplevel - from . import scripts + from . import scripts # noqa: PLC0415 return scripts.run(args.script) @@ -188,8 +185,7 @@ def main() -> int: ensure_config_path(config_dir) - # pylint: disable-next=import-outside-toplevel - from . import config, runner + from . import config, runner # noqa: PLC0415 safe_mode = config.safe_mode_enabled(config_dir) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index b60a3012aac..978758bebb1 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -52,28 +52,28 @@ _LOGGER = logging.getLogger(__name__) def _generate_secret() -> str: """Generate a secret.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 return str(pyotp.random_base32()) def _generate_random() -> int: """Generate a 32 digit number.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 return int(pyotp.random_base32(length=32, chars=list("1234567890"))) def _generate_otp(secret: str, count: int) -> str: """Generate one time password.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 return str(pyotp.HOTP(secret).at(count)) def _verify_otp(secret: str, otp: str, count: int) -> bool: """Verify one time password.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 return bool(pyotp.HOTP(secret).verify(otp, count)) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 625b273f39a..b344043b832 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -37,7 +37,7 @@ DUMMY_SECRET = "FPPTH34D4E3MI2HG" def _generate_qr_code(data: str) -> str: """Generate a base64 PNG string represent QR Code image of data.""" - import pyqrcode # pylint: disable=import-outside-toplevel + import pyqrcode # noqa: PLC0415 qr_code = pyqrcode.create(data) @@ -59,7 +59,7 @@ def _generate_qr_code(data: str) -> str: def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]: """Generate a secret, url, and QR code.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 ota_secret = pyotp.random_base32() url = pyotp.totp.TOTP(ota_secret).provisioning_uri( @@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule): def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: """Create a ota_secret for user.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 ota_secret: str = secret or pyotp.random_base32() @@ -163,7 +163,7 @@ class TotpAuthModule(MultiFactorAuthModule): def _validate_2fa(self, user_id: str, code: str) -> bool: """Validate two factor authentication code.""" - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr] # even we cannot find user, we still do verify @@ -196,7 +196,7 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]): Return self.async_show_form(step_id='init') if user_input is None. Return self.async_create_entry(data={'result': result}) if finish. """ - import pyotp # pylint: disable=import-outside-toplevel + import pyotp # noqa: PLC0415 errors: dict[str, str] = {} diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 55aeaef2554..810c1f1e8d2 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -394,7 +394,7 @@ async def async_setup_hass( def open_hass_ui(hass: core.HomeAssistant) -> None: """Open the UI.""" - import webbrowser # pylint: disable=import-outside-toplevel + import webbrowser # noqa: PLC0415 if hass.config.api is None or "frontend" not in hass.config.components: _LOGGER.warning("Cannot launch the UI because frontend not loaded") @@ -561,8 +561,7 @@ async def async_enable_logging( if not log_no_color: try: - # pylint: disable-next=import-outside-toplevel - from colorlog import ColoredFormatter + from colorlog import ColoredFormatter # noqa: PLC0415 # basicConfig must be called after importing colorlog in order to # ensure that the handlers it sets up wraps the correct streams. @@ -606,7 +605,7 @@ async def async_enable_logging( ) threading.excepthook = lambda args: logging.getLogger().exception( "Uncaught thread exception", - exc_info=( # type: ignore[arg-type] + exc_info=( # type: ignore[arg-type] # noqa: LOG014 args.exc_type, args.exc_value, args.exc_traceback, @@ -1060,5 +1059,5 @@ async def _async_setup_multi_components( _LOGGER.error( "Error setting up integration %s - received exception", domain, - exc_info=(type(result), result, result.__traceback__), + exc_info=(type(result), result, result.__traceback__), # noqa: LOG014 ) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index de60ef84efa..19ebb096a31 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -39,14 +39,14 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() try: - location_point_valid = await test_location( + location_point_valid = await check_location( websession, user_input["api_key"], user_input["latitude"], user_input["longitude"], ) if not location_point_valid: - location_nearest_valid = await test_location( + location_nearest_valid = await check_location( websession, user_input["api_key"], user_input["latitude"], @@ -88,7 +88,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN): ) -async def test_location( +async def check_location( client: ClientSession, api_key: str, latitude: float, diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index c529fbd504e..d90054252a4 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -12,7 +12,7 @@ DATA_BLUEPRINTS = "automation_blueprints" def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: """Return True if any automation references the blueprint.""" - from . import automations_with_blueprint # pylint: disable=import-outside-toplevel + from . import automations_with_blueprint # noqa: PLC0415 return len(automations_with_blueprint(hass, blueprint_path)) > 0 @@ -28,8 +28,7 @@ async def _reload_blueprint_automations( @callback def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get automation blueprints.""" - # pylint: disable-next=import-outside-toplevel - from .config import AUTOMATION_BLUEPRINT_SCHEMA + from .config import AUTOMATION_BLUEPRINT_SCHEMA # noqa: PLC0415 return blueprint.DomainBlueprints( hass, diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index daf9337a8a8..51503230530 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -94,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not with_hassio: reader_writer = CoreBackupReaderWriter(hass) else: - # pylint: disable-next=import-outside-toplevel, hass-component-root-import - from homeassistant.components.hassio.backup import SupervisorBackupReaderWriter + # pylint: disable-next=hass-component-root-import + from homeassistant.components.hassio.backup import ( # noqa: PLC0415 + SupervisorBackupReaderWriter, + ) reader_writer = SupervisorBackupReaderWriter(hass) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index df5771fe5bb..3d84d6edd69 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -54,10 +54,10 @@ class Control4RuntimeData: type Control4ConfigEntry = ConfigEntry[Control4RuntimeData] -async def call_c4_api_retry(func, *func_args): +async def call_c4_api_retry(func, *func_args): # noqa: RET503 """Call C4 API function and retry on failure.""" # Ruff doesn't understand this loop - the exception is always raised after the retries - for i in range(API_RETRY_TIMES): # noqa: RET503 + for i in range(API_RETRY_TIMES): try: return await func(*func_args) except client_exceptions.ClientError as exception: diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index fff2c00641f..cf62704b34d 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -271,7 +271,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) # Temporary migration. We can remove this in 2024.10 - from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel + from homeassistant.components.assist_pipeline import ( # noqa: PLC0415 async_migrate_engine, ) diff --git a/homeassistant/components/downloader/services.py b/homeassistant/components/downloader/services.py index 7f651c6b1f9..cce8c9d65b0 100644 --- a/homeassistant/components/downloader/services.py +++ b/homeassistant/components/downloader/services.py @@ -108,8 +108,7 @@ def download_file(service: ServiceCall) -> None: _LOGGER.debug("%s -> %s", url, final_path) with open(final_path, "wb") as fil: - for chunk in req.iter_content(1024): - fil.write(chunk) + fil.writelines(req.iter_content(1024)) _LOGGER.debug("Downloading of %s done", url) service.hass.bus.fire( diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 5f879edf005..a12af89aca2 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -63,9 +63,7 @@ class ESPHomeDashboardManager: if not (data := self._data) or not (info := data.get("info")): return if is_hassio(self._hass): - from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel - get_addons_info, - ) + from homeassistant.components.hassio import get_addons_info # noqa: PLC0415 if (addons := get_addons_info(self._hass)) is not None and info[ "addon_slug" diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9a0627f9f42..9694c299b23 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -364,8 +364,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" # Keep import here so that we can import frontend without installing reqs - # pylint: disable-next=import-outside-toplevel - import hass_frontend + import hass_frontend # noqa: PLC0415 return hass_frontend.where() diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 4309a99c0ca..6d4c9e1d219 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -212,8 +212,7 @@ class AbstractConfig(ABC): def async_enable_report_state(self) -> None: """Enable proactive mode.""" # Circular dep - # pylint: disable-next=import-outside-toplevel - from .report_state import async_enable_report_state + from .report_state import async_enable_report_state # noqa: PLC0415 if self._unsub_report_state is None: self._unsub_report_state = async_enable_report_state(self.hass, self) @@ -395,8 +394,7 @@ class AbstractConfig(ABC): async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" # Circular dep - # pylint: disable-next=import-outside-toplevel - from . import smart_home + from . import smart_home # noqa: PLC0415 self._local_last_active = utcnow() @@ -655,8 +653,9 @@ class GoogleEntity: if "matter" in self.hass.config.components and any( x for x in device_entry.identifiers if x[0] == "matter" ): - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.matter import get_matter_device_info + from homeassistant.components.matter import ( # noqa: PLC0415 + get_matter_device_info, + ) # Import matter can block the event loop for multiple seconds # so we import it here to avoid blocking the event loop during diff --git a/homeassistant/components/hassio/update_helper.py b/homeassistant/components/hassio/update_helper.py index 65a3ba38485..f44ee0700fc 100644 --- a/homeassistant/components/hassio/update_helper.py +++ b/homeassistant/components/hassio/update_helper.py @@ -29,8 +29,7 @@ async def update_addon( client = get_supervisor_client(hass) if backup: - # pylint: disable-next=import-outside-toplevel - from .backup import backup_addon_before_update + from .backup import backup_addon_before_update # noqa: PLC0415 await backup_addon_before_update(hass, addon, addon_name, installed_version) @@ -50,8 +49,7 @@ async def update_core(hass: HomeAssistant, version: str | None, backup: bool) -> client = get_supervisor_client(hass) if backup: - # pylint: disable-next=import-outside-toplevel - from .backup import backup_core_before_update + from .backup import backup_core_before_update # noqa: PLC0415 await backup_core_before_update(hass) @@ -71,8 +69,7 @@ async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> N client = get_supervisor_client(hass) if backup: - # pylint: disable-next=import-outside-toplevel - from .backup import backup_core_before_update + from .backup import backup_core_before_update # noqa: PLC0415 await backup_core_before_update(hass) diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 2b08031405f..294ed83bad1 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -309,8 +309,7 @@ class OptionsFlowHandler(OptionsFlow, ABC): def __init__(self, config_entry: ConfigEntry) -> None: """Set up the options flow.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.zha.radio_manager import ( + from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415 ZhaMultiPANMigrationHelper, ) @@ -451,16 +450,11 @@ class OptionsFlowHandler(OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Configure the Silicon Labs Multiprotocol add-on.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN - - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.zha.radio_manager import ( + from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415 + from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415 ZhaMultiPANMigrationHelper, ) - - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.zha.silabs_multiprotocol import ( + from homeassistant.components.zha.silabs_multiprotocol import ( # noqa: PLC0415 async_get_channel as async_get_zha_channel, ) @@ -747,11 +741,8 @@ class OptionsFlowHandler(OptionsFlow, ABC): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Perform initial backup and reconfigure ZHA.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN - - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.zha.radio_manager import ( + from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415 + from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415 ZhaMultiPANMigrationHelper, ) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 0acf57fe55b..df6d4498f9c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -355,11 +355,10 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="ignored_model") # Late imports in case BLE is not available - # pylint: disable-next=import-outside-toplevel - from aiohomekit.controller.ble.discovery import BleDiscovery - - # pylint: disable-next=import-outside-toplevel - from aiohomekit.controller.ble.manufacturer_data import HomeKitAdvertisement + from aiohomekit.controller.ble.discovery import BleDiscovery # noqa: PLC0415 + from aiohomekit.controller.ble.manufacturer_data import ( # noqa: PLC0415 + HomeKitAdvertisement, + ) mfr_data = discovery_info.manufacturer_data diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 2c4b67e6c99..cdf3347e24f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -278,8 +278,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ssl_certificate is not None and (hass.config.external_url or hass.config.internal_url) is None ): - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.cloud import ( + from homeassistant.components.cloud import ( # noqa: PLC0415 CloudNotAvailable, async_remote_ui_url, ) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 821d44eebaa..71f3d54bef6 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -136,8 +136,7 @@ async def process_wrong_login(request: Request) -> None: _LOGGER.warning(log_msg) # Circular import with websocket_api - # pylint: disable=import-outside-toplevel - from homeassistant.components import persistent_notification + from homeassistant.components import persistent_notification # noqa: PLC0415 persistent_notification.async_create( hass, notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN diff --git a/homeassistant/components/intent/timers.py b/homeassistant/components/intent/timers.py index d641f8dc6b5..06be933ba6b 100644 --- a/homeassistant/components/intent/timers.py +++ b/homeassistant/components/intent/timers.py @@ -444,8 +444,9 @@ class TimerManager: timer.finish() if timer.conversation_command: - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.conversation import async_converse + from homeassistant.components.conversation import ( # noqa: PLC0415 + async_converse, + ) self.hass.async_create_background_task( async_converse( diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ae010bf18c9..9e3dc59f852 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -354,8 +354,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def write_dump() -> None: with open(hass.config.path("mqtt_dump.txt"), "w", encoding="utf8") as fp: - for msg in messages: - fp.write(",".join(msg) + "\n") + fp.writelines([",".join(msg) + "\n" for msg in messages]) async def finish_dump(_: datetime) -> None: """Write dump to file.""" @@ -608,8 +607,7 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry ) -> bool: """Remove MQTT config entry from a device.""" - # pylint: disable-next=import-outside-toplevel - from . import device_automation + from . import device_automation # noqa: PLC0415 await device_automation.async_removed_from_device(hass, device_entry.id) return True diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index c2bcb306d0b..5d2b422a909 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -293,10 +293,9 @@ class MqttClientSetup: """ # We don't import on the top because some integrations # should be able to optionally rely on MQTT. - from paho.mqtt import client as mqtt # pylint: disable=import-outside-toplevel + from paho.mqtt import client as mqtt # noqa: PLC0415 - # pylint: disable-next=import-outside-toplevel - from .async_client import AsyncMQTTClient + from .async_client import AsyncMQTTClient # noqa: PLC0415 config = self._config clean_session: bool | None = None @@ -524,8 +523,7 @@ class MQTT: """Start the misc periodic.""" assert self._misc_timer is None, "Misc periodic already started" _LOGGER.debug("%s: Starting client misc loop", self.config_entry.title) - # pylint: disable=import-outside-toplevel - import paho.mqtt.client as mqtt + import paho.mqtt.client as mqtt # noqa: PLC0415 # Inner function to avoid having to check late import # each time the function is called. @@ -665,8 +663,7 @@ class MQTT: async def async_connect(self, client_available: asyncio.Future[bool]) -> None: """Connect to the host. Does not process messages yet.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt + import paho.mqtt.client as mqtt # noqa: PLC0415 result: int | None = None self._available_future = client_available @@ -724,8 +721,7 @@ class MQTT: async def _reconnect_loop(self) -> None: """Reconnect to the MQTT server.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt + import paho.mqtt.client as mqtt # noqa: PLC0415 while True: if not self.connected: @@ -1228,7 +1224,7 @@ class MQTT: """Handle a callback exception.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt # noqa: PLC0415 _LOGGER.warning( "Error returned from MQTT server: %s", @@ -1273,8 +1269,7 @@ class MQTT: ) -> None: """Wait for ACK from broker or raise on error.""" if result_code != 0: - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt + import paho.mqtt.client as mqtt # noqa: PLC0415 raise HomeAssistantError( translation_domain=DOMAIN, @@ -1322,8 +1317,7 @@ class MQTT: def _matcher_for_topic(subscription: str) -> Callable[[str], bool]: - # pylint: disable-next=import-outside-toplevel - from paho.mqtt.matcher import MQTTMatcher + from paho.mqtt.matcher import MQTTMatcher # noqa: PLC0415 matcher = MQTTMatcher() # type: ignore[no-untyped-call] matcher[subscription] = True diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index b41e549093d..ca15a899c01 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3493,7 +3493,7 @@ def try_connection( """Test if we can connect to an MQTT broker.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt # noqa: PLC0415 mqtt_client_setup = MqttClientSetup(user_input) mqtt_client_setup.setup() diff --git a/homeassistant/components/mqtt/entity.py b/homeassistant/components/mqtt/entity.py index 1202f04ed42..b62d42a80d0 100644 --- a/homeassistant/components/mqtt/entity.py +++ b/homeassistant/components/mqtt/entity.py @@ -640,8 +640,7 @@ async def cleanup_device_registry( entities, triggers or tags. """ # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import device_trigger, tag + from . import device_trigger, tag # noqa: PLC0415 device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index e3996c80a8a..1bf743d3da7 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -163,16 +163,14 @@ async def async_forward_entry_setup_and_setup_discovery( tasks: list[asyncio.Task] = [] if "device_automation" in new_platforms: # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import device_automation + from . import device_automation # noqa: PLC0415 tasks.append( create_eager_task(device_automation.async_setup_entry(hass, config_entry)) ) if "tag" in new_platforms: # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import tag + from . import tag # noqa: PLC0415 tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry))) if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 14c7dc55cf0..dd5344faa56 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -175,9 +175,7 @@ async def async_get_announce_addresses(hass: HomeAssistant) -> list[str]: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up network for Home Assistant.""" # Avoid circular issue: http->network->websocket_api->http - from .websocket import ( # pylint: disable=import-outside-toplevel - async_register_websocket_commands, - ) + from .websocket import async_register_websocket_commands # noqa: PLC0415 await async_get_network(hass) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 46538aad921..f5703022e12 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -282,8 +282,7 @@ class BaseNotificationService: for name, target in self.targets.items(): target_name = slugify(f"{self._target_service_name_prefix}_{name}") - if target_name in stale_targets: - stale_targets.remove(target_name) + stale_targets.discard(target_name) if ( target_name in self.registered_targets and target == self.registered_targets[target_name] diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index e304a39f061..1717d0b24b2 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -322,8 +322,9 @@ class OllamaConversationEntity( num_keep = 2 * max_messages + 1 drop_index = len(message_history.messages) - num_keep message_history.messages = [ - message_history.messages[0] - ] + message_history.messages[drop_index:] + message_history.messages[0], + *message_history.messages[drop_index:], + ] async def _async_entry_update_listener( self, hass: HomeAssistant, entry: ConfigEntry diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index a42577b9f34..a897d04562f 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -218,8 +218,7 @@ class UserOnboardingView(_BaseOnboardingStepView): # Return authorization code for fetching tokens and connect # during onboarding. - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.auth import create_auth_code + from homeassistant.components.auth import create_auth_code # noqa: PLC0415 auth_code = create_auth_code(hass, data["client_id"], credentials) return self.json({"auth_code": auth_code}) @@ -309,8 +308,7 @@ class IntegrationOnboardingView(_BaseOnboardingStepView): ) # Return authorization code so we can redirect user and log them in - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.auth import create_auth_code + from homeassistant.components.auth import create_auth_code # noqa: PLC0415 auth_code = create_auth_code( hass, data["client_id"], refresh_token.credential diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index de14dc30d54..749b73e5aee 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -166,7 +166,7 @@ async def async_setup_entry( # noqa: C901 # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - import objgraph # pylint: disable=import-outside-toplevel + import objgraph # noqa: PLC0415 obj_type = call.data[CONF_TYPE] @@ -192,7 +192,7 @@ async def async_setup_entry( # noqa: C901 # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - import objgraph # pylint: disable=import-outside-toplevel + import objgraph # noqa: PLC0415 for lru in objgraph.by_type(_LRU_CACHE_WRAPPER_OBJECT): lru = cast(_lru_cache_wrapper, lru) @@ -399,7 +399,7 @@ async def _async_generate_profile(hass: HomeAssistant, call: ServiceCall): # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - import cProfile # pylint: disable=import-outside-toplevel + import cProfile # noqa: PLC0415 start_time = int(time.time() * 1000000) persistent_notification.async_create( @@ -436,7 +436,7 @@ async def _async_generate_memory_profile(hass: HomeAssistant, call: ServiceCall) # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - from guppy import hpy # pylint: disable=import-outside-toplevel + from guppy import hpy # noqa: PLC0415 start_time = int(time.time() * 1000000) persistent_notification.async_create( @@ -467,7 +467,7 @@ def _write_profile(profiler, cprofile_path, callgrind_path): # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - from pyprof2calltree import convert # pylint: disable=import-outside-toplevel + from pyprof2calltree import convert # noqa: PLC0415 profiler.create_stats() profiler.dump_stats(cprofile_path) @@ -482,14 +482,14 @@ def _log_objects(*_): # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - import objgraph # pylint: disable=import-outside-toplevel + import objgraph # noqa: PLC0415 _LOGGER.critical("Memory Growth: %s", objgraph.growth(limit=1000)) def _get_function_absfile(func: Any) -> str | None: """Get the absolute file path of a function.""" - import inspect # pylint: disable=import-outside-toplevel + import inspect # noqa: PLC0415 abs_file: str | None = None with suppress(Exception): @@ -510,7 +510,7 @@ def _safe_repr(obj: Any) -> str: def _find_backrefs_not_to_self(_object: Any) -> list[str]: - import objgraph # pylint: disable=import-outside-toplevel + import objgraph # noqa: PLC0415 return [ _safe_repr(backref) @@ -526,7 +526,7 @@ def _log_object_sources( # Imports deferred to avoid loading modules # in memory since usually only one part of this # integration is used at a time - import gc # pylint: disable=import-outside-toplevel + import gc # noqa: PLC0415 gc.collect() diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index cf3addd4f20..e14a165f81f 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -242,7 +242,7 @@ def correct_db_schema_utf8( f"{table_name}.4-byte UTF-8" in schema_errors or f"{table_name}.utf8mb4_unicode_ci" in schema_errors ): - from ..migration import ( # pylint: disable=import-outside-toplevel + from ..migration import ( # noqa: PLC0415 _correct_table_character_set_and_collation, ) @@ -258,9 +258,7 @@ def correct_db_schema_precision( table_name = table_object.__tablename__ if f"{table_name}.double precision" in schema_errors: - from ..migration import ( # pylint: disable=import-outside-toplevel - _modify_columns, - ) + from ..migration import _modify_columns # noqa: PLC0415 precision_columns = _get_precision_column_types(table_object) # Attempt to convert timestamp columns to µs precision diff --git a/homeassistant/components/recorder/history/__init__.py b/homeassistant/components/recorder/history/__init__.py index a28027adb1a..469d6694640 100644 --- a/homeassistant/components/recorder/history/__init__.py +++ b/homeassistant/components/recorder/history/__init__.py @@ -45,7 +45,7 @@ def get_full_significant_states_with_session( ) -> dict[str, list[State]]: """Return a dict of significant states during a time period.""" if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # pylint: disable=import-outside-toplevel + from .legacy import ( # noqa: PLC0415 get_full_significant_states_with_session as _legacy_get_full_significant_states_with_session, ) @@ -70,7 +70,7 @@ def get_last_state_changes( ) -> dict[str, list[State]]: """Return the last number_of_states.""" if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # pylint: disable=import-outside-toplevel + from .legacy import ( # noqa: PLC0415 get_last_state_changes as _legacy_get_last_state_changes, ) @@ -94,7 +94,7 @@ def get_significant_states( ) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # pylint: disable=import-outside-toplevel + from .legacy import ( # noqa: PLC0415 get_significant_states as _legacy_get_significant_states, ) @@ -130,7 +130,7 @@ def get_significant_states_with_session( ) -> dict[str, list[State | dict[str, Any]]]: """Return a dict of significant states during a time period.""" if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # pylint: disable=import-outside-toplevel + from .legacy import ( # noqa: PLC0415 get_significant_states_with_session as _legacy_get_significant_states_with_session, ) @@ -164,7 +164,7 @@ def state_changes_during_period( ) -> dict[str, list[State]]: """Return a list of states that changed during a time period.""" if not get_instance(hass).states_meta_manager.active: - from .legacy import ( # pylint: disable=import-outside-toplevel + from .legacy import ( # noqa: PLC0415 state_changes_during_period as _legacy_state_changes_during_period, ) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 30e277d7c0a..d8d7ddb832a 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -90,7 +90,7 @@ class RecorderPool(SingletonThreadPool, NullPool): if threading.get_ident() in self.recorder_and_worker_thread_ids: super().dispose() - def _do_get(self) -> ConnectionPoolEntry: # type: ignore[return] + def _do_get(self) -> ConnectionPoolEntry: # type: ignore[return] # noqa: RET503 if threading.get_ident() in self.recorder_and_worker_thread_ids: return super()._do_get() try: @@ -100,7 +100,7 @@ class RecorderPool(SingletonThreadPool, NullPool): # which is allowed but discouraged since its much slower return self._do_get_db_connection_protected() # In the event loop, raise an exception - raise_for_blocking_call( # noqa: RET503 + raise_for_blocking_call( self._do_get_db_connection_protected, strict=True, advise_msg=ADVISE_MSG, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7f41358dddf..7326519b14e 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -2855,7 +2855,7 @@ def cleanup_statistics_timestamp_migration(instance: Recorder) -> bool: # to indicate we need to run again return False - from .migration import _drop_index # pylint: disable=import-outside-toplevel + from .migration import _drop_index # noqa: PLC0415 for table in STATISTICS_TABLES: _drop_index(instance.get_session, table, f"ix_{table}_start") diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b7b1a8e17a3..cff3e868def 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -258,7 +258,7 @@ def basic_sanity_check(cursor: SQLiteCursor) -> bool: def validate_sqlite_database(dbpath: str) -> bool: """Run a quick check on an sqlite database to see if it is corrupt.""" - import sqlite3 # pylint: disable=import-outside-toplevel + import sqlite3 # noqa: PLC0415 try: conn = sqlite3.connect(dbpath) @@ -402,9 +402,8 @@ def _datetime_or_none(value: str) -> datetime | None: def build_mysqldb_conv() -> dict: """Build a MySQLDB conv dict that uses cisco8601 to parse datetimes.""" # Late imports since we only call this if they are using mysqldb - # pylint: disable=import-outside-toplevel - from MySQLdb.constants import FIELD_TYPE - from MySQLdb.converters import conversions + from MySQLdb.constants import FIELD_TYPE # noqa: PLC0415 + from MySQLdb.converters import conversions # noqa: PLC0415 return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 92f4f5a0434..52437cc00be 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -264,8 +264,7 @@ class RMVDepartureData: for dest in self._destinations: if dest in journey["stops"]: dest_found = True - if dest in _deps_not_found: - _deps_not_found.remove(dest) + _deps_not_found.discard(dest) _nextdep["destination"] = dest if not dest_found: diff --git a/homeassistant/components/script/helpers.py b/homeassistant/components/script/helpers.py index 31aac506b35..53228517b18 100644 --- a/homeassistant/components/script/helpers.py +++ b/homeassistant/components/script/helpers.py @@ -12,7 +12,7 @@ DATA_BLUEPRINTS = "script_blueprints" def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool: """Return True if any script references the blueprint.""" - from . import scripts_with_blueprint # pylint: disable=import-outside-toplevel + from . import scripts_with_blueprint # noqa: PLC0415 return len(scripts_with_blueprint(hass, blueprint_path)) > 0 diff --git a/homeassistant/components/ssdp/server.py b/homeassistant/components/ssdp/server.py index 3a164fa374b..b6e105b9560 100644 --- a/homeassistant/components/ssdp/server.py +++ b/homeassistant/components/ssdp/server.py @@ -97,7 +97,7 @@ async def _async_find_next_available_port(source: AddressTupleVXType) -> int: test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) for port in range(UPNP_SERVER_MIN_PORT, UPNP_SERVER_MAX_PORT): - addr = (source[0],) + (port,) + source[2:] + addr = (source[0], port, *source[2:]) try: test_socket.bind(addr) except OSError: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 8fa4c69ac5a..9426b5b04de 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -119,7 +119,7 @@ def _check_stream_client_error( Raise StreamOpenClientError if an http client error is encountered. """ - from .worker import try_open_stream # pylint: disable=import-outside-toplevel + from .worker import try_open_stream # noqa: PLC0415 pyav_options, _ = _convert_stream_options(hass, source, options or {}) try: @@ -234,7 +234,7 @@ CONFIG_SCHEMA = vol.Schema( def set_pyav_logging(enable: bool) -> None: """Turn PyAV logging on or off.""" - import av # pylint: disable=import-outside-toplevel + import av # noqa: PLC0415 av.logging.set_level(av.logging.VERBOSE if enable else av.logging.FATAL) @@ -267,8 +267,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await hass.async_add_executor_job(set_pyav_logging, debug_enabled) # Keep import here so that we can import stream integration without installing reqs - # pylint: disable-next=import-outside-toplevel - from .recorder import async_setup_recorder + from .recorder import async_setup_recorder # noqa: PLC0415 hass.data[DOMAIN] = {} hass.data[DOMAIN][ATTR_ENDPOINTS] = {} @@ -460,8 +459,7 @@ class Stream: def _run_worker(self) -> None: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable-next=import-outside-toplevel - from .worker import StreamState, stream_worker + from .worker import StreamState, stream_worker # noqa: PLC0415 stream_state = StreamState(self.hass, self.outputs, self._diagnostics) wait_timeout = 0 @@ -556,8 +554,7 @@ class Stream: """Make a .mp4 recording from a provided stream.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable-next=import-outside-toplevel - from .recorder import RecorderOutput + from .recorder import RecorderOutput # noqa: PLC0415 # Check for file access if not self.hass.config.is_allowed_path(video_path): diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index b804055a740..44dfe2c323d 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -439,8 +439,9 @@ class KeyFrameConverter: # Keep import here so that we can import stream integration # without installing reqs - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.camera.img_util import TurboJPEGSingleton + from homeassistant.components.camera.img_util import ( # noqa: PLC0415 + TurboJPEGSingleton, + ) self._packet: Packet | None = None self._event: asyncio.Event = asyncio.Event() @@ -471,8 +472,7 @@ class KeyFrameConverter: # Keep import here so that we can import stream integration without # installing reqs - # pylint: disable-next=import-outside-toplevel - from av import CodecContext + from av import CodecContext # noqa: PLC0415 self._codec_context = cast( "VideoCodecContext", CodecContext.create(codec_context.name, "r") diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 5080678e3ca..3d2c40c752b 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -146,11 +146,11 @@ def get_codec_string(mp4_bytes: bytes) -> str: return ",".join(codecs) -def find_moov(mp4_io: BufferedIOBase) -> int: +def find_moov(mp4_io: BufferedIOBase) -> int: # noqa: RET503 """Find location of moov atom in a BufferedIOBase mp4.""" index = 0 # Ruff doesn't understand this loop - the exception is always raised at the end - while 1: # noqa: RET503 + while 1: mp4_io.seek(index) box_header = mp4_io.read(8) if len(box_header) != 8 or box_header[0:4] == b"\x00\x00\x00\x00": diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 37e9ee3d929..7ab6d77e137 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -231,7 +231,7 @@ async def handle_info( "Error fetching system info for %s - %s", domain, key, - exc_info=(type(exception), exception, exception.__traceback__), + exc_info=(type(exception), exception, exception.__traceback__), # noqa: LOG014 ) event_msg["success"] = False event_msg["error"] = {"type": "failed", "error": "unknown"} diff --git a/homeassistant/components/template/helpers.py b/homeassistant/components/template/helpers.py index 660227f65dc..2cd587de5a1 100644 --- a/homeassistant/components/template/helpers.py +++ b/homeassistant/components/template/helpers.py @@ -54,8 +54,7 @@ async def _reload_blueprint_templates(hass: HomeAssistant, blueprint_path: str) @callback def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: """Get template blueprints.""" - # pylint: disable-next=import-outside-toplevel - from .config import TEMPLATE_BLUEPRINT_SCHEMA + from .config import TEMPLATE_BLUEPRINT_SCHEMA # noqa: PLC0415 return blueprint.DomainBlueprints( hass, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index fa393c76ab4..10870462bc9 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -536,7 +536,6 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): effect, self.entity_id, self._effect_list, - exc_info=True, ) common_params["effect"] = effect diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 05be56d444d..696bc40fd2d 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -156,9 +156,8 @@ def setup_platform( # These imports shouldn't be moved to the top, because they depend on code from the model_dir. # (The model_dir is created during the manual setup process. See integration docs.) - # pylint: disable=import-outside-toplevel - from object_detection.builders import model_builder - from object_detection.utils import config_util, label_map_util + from object_detection.builders import model_builder # noqa: PLC0415 + from object_detection.utils import config_util, label_map_util # noqa: PLC0415 except ImportError: _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " @@ -169,7 +168,7 @@ def setup_platform( try: # Display warning that PIL will be used if no OpenCV is found. - import cv2 # noqa: F401 pylint: disable=import-outside-toplevel + import cv2 # noqa: F401, PLC0415 except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " @@ -354,7 +353,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): start = time.perf_counter() try: - import cv2 # pylint: disable=import-outside-toplevel + import cv2 # noqa: PLC0415 img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) inp = img[:, :, [2, 1, 0]] # BGR->RGB diff --git a/homeassistant/components/thread/diagnostics.py b/homeassistant/components/thread/diagnostics.py index e6149214af4..c66aec3bac9 100644 --- a/homeassistant/components/thread/diagnostics.py +++ b/homeassistant/components/thread/diagnostics.py @@ -117,9 +117,7 @@ def _get_neighbours(ndb: NDB) -> dict[str, Neighbour]: def _get_routes_and_neighbors(): """Get the routes and neighbours from pyroute2.""" # Import in the executor since import NDB can take a while - from pyroute2 import ( # pylint: disable=no-name-in-module, import-outside-toplevel - NDB, - ) + from pyroute2 import NDB # pylint: disable=no-name-in-module # noqa: PLC0415 with NDB() as ndb: routes, reverse_routes = _get_possible_thread_routes(ndb) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index cc35b1fd142..967853da629 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -317,8 +317,7 @@ class TPLinkSensorEntity(CoordinatedTPLinkFeatureEntity, SensorEntity): value = self.entity_description.convert_fn(value) if TYPE_CHECKING: - # pylint: disable-next=import-outside-toplevel - from datetime import date, datetime + from datetime import date, datetime # noqa: PLC0415 assert isinstance(value, str | int | float | date | datetime | None) diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index 91192fdca13..4ff4f93d9cd 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -40,7 +40,7 @@ def generate_media_source_id( cache: bool | None = None, ) -> str: """Generate a media source ID for text-to-speech.""" - from . import async_resolve_engine # pylint: disable=import-outside-toplevel + from . import async_resolve_engine # noqa: PLC0415 if (engine := async_resolve_engine(hass, engine)) is None: raise HomeAssistantError("Invalid TTS provider selected") @@ -193,7 +193,7 @@ class TTSMediaSource(MediaSource): @callback def _engine_item(self, engine: str, params: str | None = None) -> BrowseMediaSource: """Return provider item.""" - from . import TextToSpeechEntity # pylint: disable=import-outside-toplevel + from . import TextToSpeechEntity # noqa: PLC0415 if (engine_instance := get_engine_instance(self.hass, engine)) is None: raise BrowseError("Unknown provider") diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 32119add5f4..106075e9314 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -94,7 +94,7 @@ class SharingMQCompat(SharingMQ): """Start the MQTT client.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt # noqa: PLC0415 mqttc = mqtt.Client(client_id=mq_config.client_id) mqttc.username_pw_set(mq_config.username, mq_config.password) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 9c371a8399d..498a986e806 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -735,8 +735,7 @@ async def handle_subscribe_trigger( ) -> None: """Handle subscribe trigger command.""" # Circular dep - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers import trigger + from homeassistant.helpers import trigger # noqa: PLC0415 trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"]) @@ -786,8 +785,7 @@ async def handle_test_condition( ) -> None: """Handle test condition command.""" # Circular dep - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers import condition + from homeassistant.helpers import condition # noqa: PLC0415 # Do static + dynamic validation of the condition config = await condition.async_validate_condition_config(hass, msg["condition"]) @@ -812,8 +810,10 @@ async def handle_execute_script( ) -> None: """Handle execute script command.""" # Circular dep - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers.script import Script, async_validate_actions_config + from homeassistant.helpers.script import ( # noqa: PLC0415 + Script, + async_validate_actions_config, + ) script_config = await async_validate_actions_config(hass, msg["sequence"]) @@ -877,8 +877,7 @@ async def handle_validate_config( ) -> None: """Handle validate config command.""" # Circular dep - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers import condition, script, trigger + from homeassistant.helpers import condition, script, trigger # noqa: PLC0415 result = {} diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index 07d897bcfd6..08097880591 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -772,7 +772,7 @@ async def websocket_device_cluster_commands( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Return a list of cluster commands.""" - import voluptuous_serialize # pylint: disable=import-outside-toplevel + import voluptuous_serialize # noqa: PLC0415 zha_gateway = get_zha_gateway(hass) ieee: EUI64 = msg[ATTR_IEEE] @@ -1080,7 +1080,7 @@ async def websocket_get_configuration( ) -> None: """Get ZHA configuration.""" config_entry: ConfigEntry = get_config_entry(hass) - import voluptuous_serialize # pylint: disable=import-outside-toplevel + import voluptuous_serialize # noqa: PLC0415 def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index b8b8662c0b5..f74357327e9 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -166,9 +166,9 @@ async def async_attach_trigger( if ( config[ATTR_PARTIAL_DICT_MATCH] and isinstance(event_data[key], dict) - and isinstance(event_data_filter[key], dict) + and isinstance(val, dict) ): - for key2, val2 in event_data_filter[key].items(): + for key2, val2 in val.items(): if key2 not in event_data[key] or event_data[key][key2] != val2: return continue diff --git a/homeassistant/config.py b/homeassistant/config.py index c3f02539f7d..ca1c87e4a11 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1321,8 +1321,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: This method is a coroutine. """ - # pylint: disable-next=import-outside-toplevel - from .helpers import check_config + from .helpers import check_config # noqa: PLC0415 res = await check_config.async_check_ha_config_file(hass) diff --git a/homeassistant/core.py b/homeassistant/core.py index afffb883741..c5d4ca79371 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -179,8 +179,7 @@ class EventStateReportedData(EventStateEventData): def _deprecated_core_config() -> Any: - # pylint: disable-next=import-outside-toplevel - from . import core_config + from . import core_config # noqa: PLC0415 return core_config.Config @@ -428,8 +427,7 @@ class HomeAssistant: def __init__(self, config_dir: str) -> None: """Initialize new Home Assistant object.""" - # pylint: disable-next=import-outside-toplevel - from .core_config import Config + from .core_config import Config # noqa: PLC0415 # This is a dictionary that any component can store any data on. self.data = HassDict() @@ -458,7 +456,7 @@ class HomeAssistant: """Report and raise if we are not running in the event loop thread.""" if self.loop_thread_id != threading.get_ident(): # frame is a circular import, so we import it here - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_non_thread_safe_operation(what) @@ -522,8 +520,7 @@ class HomeAssistant: await self.async_start() if attach_signals: - # pylint: disable-next=import-outside-toplevel - from .helpers.signal import async_register_signal_handling + from .helpers.signal import async_register_signal_handling # noqa: PLC0415 async_register_signal_handling(self) @@ -643,7 +640,7 @@ class HomeAssistant: args: parameters for method to call. """ # late import to avoid circular imports - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_usage( "calls `async_add_job`, which should be reviewed against " @@ -699,7 +696,7 @@ class HomeAssistant: args: parameters for method to call. """ # late import to avoid circular imports - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_usage( "calls `async_add_hass_job`, which should be reviewed against " @@ -802,7 +799,7 @@ class HomeAssistant: target: target to call. """ if self.loop_thread_id != threading.get_ident(): - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_non_thread_safe_operation("hass.async_create_task") return self.async_create_task_internal(target, name, eager_start) @@ -973,7 +970,7 @@ class HomeAssistant: args: parameters for method to call. """ # late import to avoid circular imports - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_usage( "calls `async_run_job`, which should be reviewed against " @@ -1517,7 +1514,7 @@ class EventBus: """ _verify_event_type_length_or_raise(event_type) if self._hass.loop_thread_id != threading.get_ident(): - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_non_thread_safe_operation("hass.bus.async_fire") return self.async_fire_internal( @@ -1622,7 +1619,7 @@ class EventBus: """ if run_immediately in (True, False): # late import to avoid circular imports - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_usage( "calls `async_listen` with run_immediately", @@ -1692,7 +1689,7 @@ class EventBus: """ if run_immediately in (True, False): # late import to avoid circular imports - from .helpers import frame # pylint: disable=import-outside-toplevel + from .helpers import frame # noqa: PLC0415 frame.report_usage( "calls `async_listen_once` with run_immediately", diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py index f1ba96daae4..5ccd8a49f32 100644 --- a/homeassistant/core_config.py +++ b/homeassistant/core_config.py @@ -538,8 +538,7 @@ class Config: def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" - # pylint: disable-next=import-outside-toplevel - from .components.zone import DEFAULT_RADIUS + from .components.zone import DEFAULT_RADIUS # noqa: PLC0415 self.hass = hass @@ -845,8 +844,7 @@ class Config: ) -> dict[str, Any]: """Migrate to the new version.""" - # pylint: disable-next=import-outside-toplevel - from .components.zone import DEFAULT_RADIUS + from .components.zone import DEFAULT_RADIUS # noqa: PLC0415 data = old_data if old_major_version == 1 and old_minor_version < 2: @@ -863,8 +861,9 @@ class Config: try: owner = await self.hass.auth.async_get_owner() if owner is not None: - # pylint: disable-next=import-outside-toplevel - from .components.frontend import storage as frontend_store + from .components.frontend import ( # noqa: PLC0415 + storage as frontend_store, + ) owner_store = await frontend_store.async_user_store( self.hass, owner.id diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 0b2d2c071c5..23416480dd7 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -23,8 +23,7 @@ def import_async_get_exception_message() -> Callable[ Defaults to English, requires translations to already be cached. """ - # pylint: disable-next=import-outside-toplevel - from .helpers.translation import ( + from .helpers.translation import ( # noqa: PLC0415 async_get_exception_message as async_get_exception_message_import, ) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index ba02ed51f6b..cfc250754ec 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -475,8 +475,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): @callback def _async_setup_cleanup(self) -> None: """Set up the area registry cleanup.""" - # pylint: disable-next=import-outside-toplevel - from . import ( # Circular dependencies + from . import ( # Circular dependencies # noqa: PLC0415 floor_registry as fr, label_registry as lr, ) @@ -543,8 +542,7 @@ def async_entries_for_label(registry: AreaRegistry, label_id: str) -> list[AreaE def _validate_temperature_entity(hass: HomeAssistant, entity_id: str) -> None: """Validate temperature entity.""" - # pylint: disable=import-outside-toplevel - from homeassistant.components.sensor import SensorDeviceClass + from homeassistant.components.sensor import SensorDeviceClass # noqa: PLC0415 if not (state := hass.states.get(entity_id)): raise ValueError(f"Entity {entity_id} does not exist") @@ -558,8 +556,7 @@ def _validate_temperature_entity(hass: HomeAssistant, entity_id: str) -> None: def _validate_humidity_entity(hass: HomeAssistant, entity_id: str) -> None: """Validate humidity entity.""" - # pylint: disable=import-outside-toplevel - from homeassistant.components.sensor import SensorDeviceClass + from homeassistant.components.sensor import SensorDeviceClass # noqa: PLC0415 if not (state := hass.states.get(entity_id)): raise ValueError(f"Entity {entity_id} does not exist") diff --git a/homeassistant/helpers/backup.py b/homeassistant/helpers/backup.py index b3607f6653c..e445bef4aae 100644 --- a/homeassistant/helpers/backup.py +++ b/homeassistant/helpers/backup.py @@ -43,8 +43,7 @@ def async_initialize_backup(hass: HomeAssistant) -> None: registers the basic backup websocket API which is used by frontend to subscribe to backup events. """ - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.backup import basic_websocket + from homeassistant.components.backup import basic_websocket # noqa: PLC0415 hass.data[DATA_BACKUP] = BackupData() basic_websocket.async_register_websocket_handlers(hass) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 45e2e7cf35f..761a9c5714e 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -222,16 +222,14 @@ class WebhookFlowHandler(config_entries.ConfigFlow): return self.async_show_form(step_id="user") # Local import to be sure cloud is loaded and setup - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.cloud import ( + from homeassistant.components.cloud import ( # noqa: PLC0415 async_active_subscription, async_create_cloudhook, async_is_connected, ) # Local import to be sure webhook is loaded and setup - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.webhook import ( + from homeassistant.components.webhook import ( # noqa: PLC0415 async_generate_id, async_generate_url, ) @@ -281,7 +279,6 @@ async def webhook_async_remove_entry( return # Local import to be sure cloud is loaded and setup - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.cloud import async_delete_cloudhook + from homeassistant.components.cloud import async_delete_cloudhook # noqa: PLC0415 await async_delete_cloudhook(hass, entry.data["webhook_id"]) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 31a3e365071..5445cb51ac9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -721,8 +721,7 @@ def template(value: Any | None) -> template_helper.Template: if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") if not (hass := _async_get_hass_or_none()): - # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import ReportBehavior, report_usage # noqa: PLC0415 report_usage( ( @@ -750,8 +749,7 @@ def dynamic_template(value: Any | None) -> template_helper.Template: if not template_helper.is_template_string(str(value)): raise vol.Invalid("template value does not contain a dynamic template") if not (hass := _async_get_hass_or_none()): - # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import ReportBehavior, report_usage # noqa: PLC0415 report_usage( ( @@ -1151,9 +1149,9 @@ def custom_serializer(schema: Any) -> Any: def _custom_serializer(schema: Any, *, allow_section: bool) -> Any: """Serialize additional types for voluptuous_serialize.""" - from homeassistant import data_entry_flow # pylint: disable=import-outside-toplevel + from homeassistant import data_entry_flow # noqa: PLC0415 - from . import selector # pylint: disable=import-outside-toplevel + from . import selector # noqa: PLC0415 if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} @@ -1216,8 +1214,7 @@ def _no_yaml_config_schema( """Return a config schema which logs if attempted to setup from YAML.""" def raise_issue() -> None: - # pylint: disable-next=import-outside-toplevel - from .issue_registry import IssueSeverity, async_create_issue + from .issue_registry import IssueSeverity, async_create_issue # noqa: PLC0415 # HomeAssistantError is raised if called from the wrong thread with contextlib.suppress(HomeAssistantError): diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 101b9731caf..20b5b7ebab9 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -190,11 +190,10 @@ def _print_deprecation_warning_internal_impl( *, log_when_no_integration_is_found: bool, ) -> None: - # pylint: disable=import-outside-toplevel - from homeassistant.core import async_get_hass_or_none - from homeassistant.loader import async_suggest_report_issue + from homeassistant.core import async_get_hass_or_none # noqa: PLC0415 + from homeassistant.loader import async_suggest_report_issue # noqa: PLC0415 - from .frame import MissingIntegrationFrame, get_integration_frame + from .frame import MissingIntegrationFrame, get_integration_frame # noqa: PLC0415 logger = logging.getLogger(module_name) if breaks_in_ha_version: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 4f36ff8ec94..a6313381492 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1018,8 +1018,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): and old.area_id is None ): # Circular dep - # pylint: disable-next=import-outside-toplevel - from . import area_registry as ar + from . import area_registry as ar # noqa: PLC0415 area = ar.async_get(self.hass).async_get_or_create(suggested_area) area_id = area.id @@ -1622,8 +1621,7 @@ def async_cleanup( @callback def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: """Clean up device registry when entities removed.""" - # pylint: disable-next=import-outside-toplevel - from . import entity_registry, label_registry as lr + from . import entity_registry, label_registry as lr # noqa: PLC0415 @callback def _label_removed_from_registry_filter( diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 0cb668a5ffd..0b61c3e8f16 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -1745,8 +1745,7 @@ def async_config_entry_disabled_by_changed( @callback def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: """Clean up device registry when entities removed.""" - # pylint: disable-next=import-outside-toplevel - from . import category_registry as cr, event, label_registry as lr + from . import category_registry as cr, event, label_registry as lr # noqa: PLC0415 @callback def _removed_from_registry_filter( diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index a97dd48bf61..176bcfcd7c4 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -235,10 +235,7 @@ def find_paths_unserializable_data( This method is slow! Only use for error handling. """ - from homeassistant.core import ( # pylint: disable=import-outside-toplevel - Event, - State, - ) + from homeassistant.core import Event, State # noqa: PLC0415 to_process = deque([(bad_data, "$")]) invalid = {} diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 1e4abb07ddb..5d9e4c3bdef 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -216,8 +216,7 @@ class APIInstance: async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType: """Call a LLM tool, validate args and return the response.""" - # pylint: disable=import-outside-toplevel - from homeassistant.components.conversation import ( + from homeassistant.components.conversation import ( # noqa: PLC0415 ConversationTraceEventType, async_conversation_trace_append, ) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 67c4448724e..6f4aadaf786 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -186,8 +186,7 @@ def get_url( known_hostnames = ["localhost"] if is_hassio(hass): # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.hassio import get_host_info + from homeassistant.components.hassio import get_host_info # noqa: PLC0415 if host_info := get_host_info(hass): known_hostnames.extend( @@ -318,8 +317,7 @@ def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) - """Get external Home Assistant Cloud URL of this instance.""" if "cloud" in hass.config.components: # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.cloud import ( + from homeassistant.components.cloud import ( # noqa: PLC0415 CloudNotAvailable, async_remote_ui_url, ) diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index 7ad319419c1..1698646d6b5 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -35,8 +35,7 @@ class RecorderData: @callback def async_migration_in_progress(hass: HomeAssistant) -> bool: """Check to see if a recorder migration is in progress.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder + from homeassistant.components import recorder # noqa: PLC0415 return recorder.util.async_migration_in_progress(hass) @@ -44,8 +43,7 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: @callback def async_migration_is_live(hass: HomeAssistant) -> bool: """Check to see if a recorder migration is live.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder + from homeassistant.components import recorder # noqa: PLC0415 return recorder.util.async_migration_is_live(hass) @@ -58,8 +56,9 @@ def async_initialize_recorder(hass: HomeAssistant) -> None: registers the basic recorder websocket API which is used by frontend to determine if the recorder is migrating the database. """ - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.recorder.basic_websocket_api import async_setup + from homeassistant.components.recorder.basic_websocket_api import ( # noqa: PLC0415 + async_setup, + ) hass.data[DATA_RECORDER] = RecorderData() async_setup(hass) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 4a10dfc5616..51d9c97ceeb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -85,8 +85,7 @@ ALL_SERVICE_DESCRIPTIONS_CACHE: HassKey[ @cache def _base_components() -> dict[str, ModuleType]: """Return a cached lookup of base components.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import ( + from homeassistant.components import ( # noqa: PLC0415 alarm_control_panel, assist_satellite, calendar, @@ -1296,8 +1295,7 @@ def async_register_entity_service( if schema is None or isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) elif not cv.is_entity_service_schema(schema): - # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import ReportBehavior, report_usage # noqa: PLC0415 report_usage( "registers an entity service with a non entity service schema", diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index fe94be68763..2dd9decb582 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -354,7 +354,7 @@ class Store[_T: Mapping[str, Any] | Sequence[Any]]: corrupt_path, err, ) - from .issue_registry import ( # pylint: disable=import-outside-toplevel + from .issue_registry import ( # noqa: PLC0415 IssueSeverity, async_create_issue, ) diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 8f5e2418b14..1c35f45d713 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -31,8 +31,8 @@ def get_astral_location( hass: HomeAssistant, ) -> tuple[astral.location.Location, astral.Elevation]: """Get an astral location for the current Home Assistant configuration.""" - from astral import LocationInfo # pylint: disable=import-outside-toplevel - from astral.location import Location # pylint: disable=import-outside-toplevel + from astral import LocationInfo # noqa: PLC0415 + from astral.location import Location # noqa: PLC0415 latitude = hass.config.latitude longitude = hass.config.longitude diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index df9679dcb08..30b7616319d 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -42,8 +42,7 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: # may not be loaded yet and we don't want to # do blocking I/O in the event loop to import it. if TYPE_CHECKING: - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import hassio + from homeassistant.components import hassio # noqa: PLC0415 else: hassio = await async_import_module(hass, "homeassistant.components.hassio") diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9079d6af300..acf78f70380 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -210,9 +210,7 @@ def async_setup(hass: HomeAssistant) -> bool: if new_size > current_size: lru.set_size(new_size) - from .event import ( # pylint: disable=import-outside-toplevel - async_track_time_interval, - ) + from .event import async_track_time_interval # noqa: PLC0415 cancel = async_track_time_interval( hass, _async_adjust_lru_sizes, timedelta(minutes=10) @@ -527,8 +525,7 @@ class Template: Note: A valid hass instance should always be passed in. The hass parameter will be non optional in Home Assistant Core 2025.10. """ - # pylint: disable-next=import-outside-toplevel - from .frame import ReportBehavior, report_usage + from .frame import ReportBehavior, report_usage # noqa: PLC0415 if not isinstance(template, str): raise TypeError("Expected template to be a string") @@ -1141,8 +1138,7 @@ class TemplateStateBase(State): def format_state(self, rounded: bool, with_unit: bool) -> str: """Return a formatted version of the state.""" # Import here, not at top-level, to avoid circular import - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.sensor import ( + from homeassistant.components.sensor import ( # noqa: PLC0415 DOMAIN as SENSOR_DOMAIN, async_rounded_state, ) @@ -1278,7 +1274,7 @@ def forgiving_boolean[_T]( """Try to convert value to a boolean.""" try: # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel + from . import config_validation as cv # noqa: PLC0415 return cv.boolean(value) except vol.Invalid: @@ -1303,7 +1299,7 @@ def result_as_boolean(template_result: Any | None) -> bool: def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: """Expand out any groups and zones into entity states.""" # circular import. - from . import entity as entity_helper # pylint: disable=import-outside-toplevel + from . import entity as entity_helper # noqa: PLC0415 search = list(args) found = {} @@ -1376,8 +1372,7 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: return entities # fallback to just returning all entities for a domain - # pylint: disable-next=import-outside-toplevel - from .entity import entity_sources + from .entity import entity_sources # noqa: PLC0415 return [ entity_id @@ -1421,7 +1416,7 @@ def device_name(hass: HomeAssistant, lookup_value: str) -> str | None: ent_reg = entity_registry.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel + from . import config_validation as cv # noqa: PLC0415 try: cv.entity_id(lookup_value) @@ -1579,7 +1574,7 @@ def area_id(hass: HomeAssistant, lookup_value: str) -> str | None: ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel + from . import config_validation as cv # noqa: PLC0415 try: cv.entity_id(lookup_value) @@ -1617,7 +1612,7 @@ def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel + from . import config_validation as cv # noqa: PLC0415 try: cv.entity_id(lookup_value) @@ -1698,7 +1693,7 @@ def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None ent_reg = entity_registry.async_get(hass) # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel + from . import config_validation as cv # noqa: PLC0415 lookup_value = str(lookup_value) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 65774a0b168..dde456bf7bc 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -41,8 +41,7 @@ def _deprecated_typing_helper(attr: str) -> DeferredDeprecatedAlias: """Help to make a DeferredDeprecatedAlias.""" def value_fn() -> Any: - # pylint: disable-next=import-outside-toplevel - import homeassistant.core + import homeassistant.core # noqa: PLC0415 return getattr(homeassistant.core, attr) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 0980a6f2ba9..6a3061b0d2a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -291,7 +291,7 @@ def _get_custom_components(hass: HomeAssistant) -> dict[str, Integration]: return {} try: - import custom_components # pylint: disable=import-outside-toplevel + import custom_components # noqa: PLC0415 except ImportError: return {} @@ -1392,7 +1392,7 @@ async def async_get_integrations( # Now the rest use resolve_from_root if needed: - from . import components # pylint: disable=import-outside-toplevel + from . import components # noqa: PLC0415 integrations = await hass.async_add_executor_job( _resolve_integrations_from_root, hass, components, needed @@ -1728,7 +1728,7 @@ def _async_mount_config_dir(hass: HomeAssistant) -> None: sys.path.insert(0, hass.config.config_dir) with suppress(ImportError): - import custom_components # pylint: disable=import-outside-toplevel # noqa: F401 + import custom_components # noqa: F401, PLC0415 sys.path.remove(hass.config.config_dir) sys.path_importer_cache.pop(hass.config.config_dir, None) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 981f0a26926..213a45a48e9 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -47,8 +47,7 @@ WARNING_STR = "General Warnings" def color(the_color, *args, reset=None): """Color helper.""" - # pylint: disable-next=import-outside-toplevel - from colorlog.escape_codes import escape_codes, parse_colors + from colorlog.escape_codes import escape_codes, parse_colors # noqa: PLC0415 try: if not args: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 39f0a7656f3..a631eb07ca2 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -101,8 +101,7 @@ def async_notify_setup_error( This method must be run in the event loop. """ - # pylint: disable-next=import-outside-toplevel - from .components import persistent_notification + from .components import persistent_notification # noqa: PLC0415 if (errors := hass.data.get(_DATA_PERSISTENT_ERRORS)) is None: errors = hass.data[_DATA_PERSISTENT_ERRORS] = {} diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index f8901d11114..593a169f75e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -36,8 +36,7 @@ def create_eager_task[_T]( # If there is no running loop, create_eager_task is being called from # the wrong thread. # Late import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from homeassistant.helpers import frame + from homeassistant.helpers import frame # noqa: PLC0415 frame.report_usage("attempted to create an asyncio task from a thread") raise diff --git a/homeassistant/util/signal_type.pyi b/homeassistant/util/signal_type.pyi index 9987c3a0931..933467c351a 100644 --- a/homeassistant/util/signal_type.pyi +++ b/homeassistant/util/signal_type.pyi @@ -31,9 +31,8 @@ def _test_signal_type_typing() -> None: # noqa: PYI048 This is tested during the mypy run. Do not move it to 'tests'! """ - # pylint: disable=import-outside-toplevel - from homeassistant.core import HomeAssistant - from homeassistant.helpers.dispatcher import ( + from homeassistant.core import HomeAssistant # noqa: PLC0415 + from homeassistant.helpers.dispatcher import ( # noqa: PLC0415 async_dispatcher_connect, async_dispatcher_send, ) diff --git a/pyproject.toml b/pyproject.toml index 83782631191..0213dbf27ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -287,6 +287,7 @@ disable = [ # "global-statement", # PLW0603, ruff catches new occurrences, needs more work "global-variable-not-assigned", # PLW0602 "implicit-str-concat", # ISC001 + "import-outside-toplevel", # PLC0415 "import-self", # PLW0406 "inconsistent-quotes", # Q000 "invalid-envvar-default", # PLW1508 @@ -812,6 +813,7 @@ ignore = [ "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW1641", # __eq__ without __hash__ "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception "PT018", # Assertion should be broken down into multiple parts @@ -835,6 +837,9 @@ ignore = [ "TRY400", # Use `logging.exception` instead of `logging.error` # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` + "UP046", # Non PEP 695 generic class + "UP047", # Non PEP 696 generic function + "UP049", # Avoid private type parameter names # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ba05be7043b..1abbf3977cf 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.4.1 -ruff==0.11.12 +ruff==0.12.0 yamllint==1.37.1 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 95966ddbdab..72bd1ab3e7d 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -27,7 +27,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ stdlib-list==0.10.0 \ pipdeptree==2.26.1 \ tqdm==4.67.1 \ - ruff==0.11.12 \ + ruff==0.12.0 \ PyTurboJPEG==1.8.0 \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ diff --git a/script/lint_and_test.py b/script/lint_and_test.py index fb350c113b9..44d9e5d8eb7 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -42,8 +42,7 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable-next=import-outside-toplevel - from gen_requirements_all import main as req_main + from gen_requirements_all import main as req_main # noqa: PLC0415 return req_main(True) == 0 diff --git a/script/version_bump.py b/script/version_bump.py index ff94c01a5a2..2a7d82937f1 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -198,7 +198,7 @@ def main() -> None: def test_bump_version() -> None: """Make sure it all works.""" - import pytest + import pytest # noqa: PLC0415 assert bump_version(Version("0.56.0"), "beta") == Version("0.56.1b0") assert bump_version(Version("0.56.0b3"), "beta") == Version("0.56.0b4") diff --git a/tests/common.py b/tests/common.py index 66129ecc9c3..322a47c8a09 100644 --- a/tests/common.py +++ b/tests/common.py @@ -452,11 +452,9 @@ def async_fire_mqtt_message( # Local import to avoid processing MQTT modules when running a testcase # which does not use MQTT. - # pylint: disable-next=import-outside-toplevel - from paho.mqtt.client import MQTTMessage + from paho.mqtt.client import MQTTMessage # noqa: PLC0415 - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.mqtt import MqttData + from homeassistant.components.mqtt import MqttData # noqa: PLC0415 if isinstance(payload, str): payload = payload.encode("utf-8") @@ -1736,8 +1734,7 @@ def async_get_persistent_notifications( def async_mock_cloud_connection_status(hass: HomeAssistant, connected: bool) -> None: """Mock a signal the cloud disconnected.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.cloud import ( + from homeassistant.components.cloud import ( # noqa: PLC0415 SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState, ) diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py index 8fffdba7cc2..b2dac6a6f8f 100644 --- a/tests/components/backup/conftest.py +++ b/tests/components/backup/conftest.py @@ -166,8 +166,7 @@ def mock_backup_generation_fixture( @pytest.fixture def mock_backups() -> Generator[None]: """Fixture to setup test backups.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.backup import backup as core_backup + from homeassistant.components.backup import backup as core_backup # noqa: PLC0415 class CoreLocalBackupAgent(core_backup.CoreLocalBackupAgent): def __init__(self, hass: HomeAssistant) -> None: diff --git a/tests/components/conftest.py b/tests/components/conftest.py index e0db306cae9..48198757c25 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -98,8 +98,9 @@ def entity_registry_enabled_by_default() -> Generator[None]: @pytest.fixture(name="stub_blueprint_populate") def stub_blueprint_populate_fixture() -> Generator[None]: """Stub copying the blueprints to the config folder.""" - # pylint: disable-next=import-outside-toplevel - from .blueprint.common import stub_blueprint_populate_fixture_helper + from .blueprint.common import ( # noqa: PLC0415 + stub_blueprint_populate_fixture_helper, + ) yield from stub_blueprint_populate_fixture_helper() @@ -108,8 +109,7 @@ def stub_blueprint_populate_fixture() -> Generator[None]: @pytest.fixture(name="mock_tts_get_cache_files") def mock_tts_get_cache_files_fixture() -> Generator[MagicMock]: """Mock the list TTS cache function.""" - # pylint: disable-next=import-outside-toplevel - from .tts.common import mock_tts_get_cache_files_fixture_helper + from .tts.common import mock_tts_get_cache_files_fixture_helper # noqa: PLC0415 yield from mock_tts_get_cache_files_fixture_helper() @@ -119,8 +119,7 @@ def mock_tts_init_cache_dir_fixture( init_tts_cache_dir_side_effect: Any, ) -> Generator[MagicMock]: """Mock the TTS cache dir in memory.""" - # pylint: disable-next=import-outside-toplevel - from .tts.common import mock_tts_init_cache_dir_fixture_helper + from .tts.common import mock_tts_init_cache_dir_fixture_helper # noqa: PLC0415 yield from mock_tts_init_cache_dir_fixture_helper(init_tts_cache_dir_side_effect) @@ -128,8 +127,9 @@ def mock_tts_init_cache_dir_fixture( @pytest.fixture(name="init_tts_cache_dir_side_effect") def init_tts_cache_dir_side_effect_fixture() -> Any: """Return the cache dir.""" - # pylint: disable-next=import-outside-toplevel - from .tts.common import init_tts_cache_dir_side_effect_fixture_helper + from .tts.common import ( # noqa: PLC0415 + init_tts_cache_dir_side_effect_fixture_helper, + ) return init_tts_cache_dir_side_effect_fixture_helper() @@ -142,8 +142,7 @@ def mock_tts_cache_dir_fixture( request: pytest.FixtureRequest, ) -> Generator[Path]: """Mock the TTS cache dir with empty dir.""" - # pylint: disable-next=import-outside-toplevel - from .tts.common import mock_tts_cache_dir_fixture_helper + from .tts.common import mock_tts_cache_dir_fixture_helper # noqa: PLC0415 yield from mock_tts_cache_dir_fixture_helper( tmp_path, mock_tts_init_cache_dir, mock_tts_get_cache_files, request @@ -153,8 +152,7 @@ def mock_tts_cache_dir_fixture( @pytest.fixture(name="tts_mutagen_mock") def tts_mutagen_mock_fixture() -> Generator[MagicMock]: """Mock writing tags.""" - # pylint: disable-next=import-outside-toplevel - from .tts.common import tts_mutagen_mock_fixture_helper + from .tts.common import tts_mutagen_mock_fixture_helper # noqa: PLC0415 yield from tts_mutagen_mock_fixture_helper() @@ -162,8 +160,9 @@ def tts_mutagen_mock_fixture() -> Generator[MagicMock]: @pytest.fixture(name="mock_conversation_agent") def mock_conversation_agent_fixture(hass: HomeAssistant) -> MockAgent: """Mock a conversation agent.""" - # pylint: disable-next=import-outside-toplevel - from .conversation.common import mock_conversation_agent_fixture_helper + from .conversation.common import ( # noqa: PLC0415 + mock_conversation_agent_fixture_helper, + ) return mock_conversation_agent_fixture_helper(hass) @@ -180,8 +179,7 @@ def prevent_ffmpeg_subprocess() -> Generator[None]: @pytest.fixture def mock_light_entities() -> list[MockLight]: """Return mocked light entities.""" - # pylint: disable-next=import-outside-toplevel - from .light.common import MockLight + from .light.common import MockLight # noqa: PLC0415 return [ MockLight("Ceiling", STATE_ON), @@ -193,8 +191,7 @@ def mock_light_entities() -> list[MockLight]: @pytest.fixture def mock_sensor_entities() -> dict[str, MockSensor]: """Return mocked sensor entities.""" - # pylint: disable-next=import-outside-toplevel - from .sensor.common import get_mock_sensor_entities + from .sensor.common import get_mock_sensor_entities # noqa: PLC0415 return get_mock_sensor_entities() @@ -202,8 +199,7 @@ def mock_sensor_entities() -> dict[str, MockSensor]: @pytest.fixture def mock_switch_entities() -> list[MockSwitch]: """Return mocked toggle entities.""" - # pylint: disable-next=import-outside-toplevel - from .switch.common import get_mock_switch_entities + from .switch.common import get_mock_switch_entities # noqa: PLC0415 return get_mock_switch_entities() @@ -211,8 +207,7 @@ def mock_switch_entities() -> list[MockSwitch]: @pytest.fixture def mock_legacy_device_scanner() -> MockScanner: """Return mocked legacy device scanner entity.""" - # pylint: disable-next=import-outside-toplevel - from .device_tracker.common import MockScanner + from .device_tracker.common import MockScanner # noqa: PLC0415 return MockScanner() @@ -220,8 +215,7 @@ def mock_legacy_device_scanner() -> MockScanner: @pytest.fixture def mock_legacy_device_tracker_setup() -> Callable[[HomeAssistant, MockScanner], None]: """Return setup callable for legacy device tracker setup.""" - # pylint: disable-next=import-outside-toplevel - from .device_tracker.common import mock_legacy_device_tracker_setup + from .device_tracker.common import mock_legacy_device_tracker_setup # noqa: PLC0415 return mock_legacy_device_tracker_setup @@ -231,8 +225,7 @@ def addon_manager_fixture( hass: HomeAssistant, supervisor_client: AsyncMock ) -> AddonManager: """Return an AddonManager instance.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_manager + from .hassio.common import mock_addon_manager # noqa: PLC0415 return mock_addon_manager(hass) @@ -288,8 +281,7 @@ def addon_store_info_fixture( addon_store_info_side_effect: Any | None, ) -> AsyncMock: """Mock Supervisor add-on store info.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_store_info + from .hassio.common import mock_addon_store_info # noqa: PLC0415 return mock_addon_store_info(supervisor_client, addon_store_info_side_effect) @@ -305,8 +297,7 @@ def addon_info_fixture( supervisor_client: AsyncMock, addon_info_side_effect: Any | None ) -> AsyncMock: """Mock Supervisor add-on info.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_info + from .hassio.common import mock_addon_info # noqa: PLC0415 return mock_addon_info(supervisor_client, addon_info_side_effect) @@ -316,8 +307,7 @@ def addon_not_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on not installed.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_not_installed + from .hassio.common import mock_addon_not_installed # noqa: PLC0415 return mock_addon_not_installed(addon_store_info, addon_info) @@ -327,8 +317,7 @@ def addon_installed_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on already installed but not running.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_installed + from .hassio.common import mock_addon_installed # noqa: PLC0415 return mock_addon_installed(addon_store_info, addon_info) @@ -338,8 +327,7 @@ def addon_running_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> AsyncMock: """Mock add-on already running.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_running + from .hassio.common import mock_addon_running # noqa: PLC0415 return mock_addon_running(addon_store_info, addon_info) @@ -350,8 +338,7 @@ def install_addon_side_effect_fixture( ) -> Any | None: """Return the install add-on side effect.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_install_addon_side_effect + from .hassio.common import mock_install_addon_side_effect # noqa: PLC0415 return mock_install_addon_side_effect(addon_store_info, addon_info) @@ -371,8 +358,7 @@ def start_addon_side_effect_fixture( addon_store_info: AsyncMock, addon_info: AsyncMock ) -> Any | None: """Return the start add-on options side effect.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_start_addon_side_effect + from .hassio.common import mock_start_addon_side_effect # noqa: PLC0415 return mock_start_addon_side_effect(addon_store_info, addon_info) @@ -419,8 +405,7 @@ def set_addon_options_side_effect_fixture( addon_options: dict[str, Any], ) -> Any | None: """Return the set add-on options side effect.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_set_addon_options_side_effect + from .hassio.common import mock_set_addon_options_side_effect # noqa: PLC0415 return mock_set_addon_options_side_effect(addon_options) @@ -446,8 +431,7 @@ def uninstall_addon_fixture(supervisor_client: AsyncMock) -> AsyncMock: @pytest.fixture(name="create_backup") def create_backup_fixture() -> Generator[AsyncMock]: """Mock create backup.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_create_backup + from .hassio.common import mock_create_backup # noqa: PLC0415 yield from mock_create_backup() @@ -486,8 +470,7 @@ def store_info_fixture( @pytest.fixture(name="addon_stats") def addon_stats_fixture(supervisor_client: AsyncMock) -> AsyncMock: """Mock addon stats info.""" - # pylint: disable-next=import-outside-toplevel - from .hassio.common import mock_addon_stats + from .hassio.common import mock_addon_stats # noqa: PLC0415 return mock_addon_stats(supervisor_client) diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 5576066f781..e9a03f9fb31 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -275,7 +275,7 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> requested_count=1, ) for parent_id, title in zip( - ["0"] + object_ids[:-1], path.split("/"), strict=False + ["0", *object_ids[:-1]], path.split("/"), strict=False ) ] assert result.url == res_abs_url @@ -293,7 +293,7 @@ async def test_resolve_media_path(hass: HomeAssistant, dms_device_mock: Mock) -> requested_count=1, ) for parent_id, title in zip( - ["0"] + object_ids[:-1], path.split("/"), strict=False + ["0", *object_ids[:-1]], path.split("/"), strict=False ) ] assert result.url == res_abs_url @@ -351,7 +351,7 @@ async def test_resolve_path_browsed(hass: HomeAssistant, dms_device_mock: Mock) requested_count=1, ) for parent_id, title in zip( - ["0"] + object_ids[:-1], path.split("/"), strict=False + ["0", *object_ids[:-1]], path.split("/"), strict=False ) ] assert result.didl_metadata.id == object_ids[-1] diff --git a/tests/components/keyboard/test_init.py b/tests/components/keyboard/test_init.py index f590c9dd1a4..69355efd761 100644 --- a/tests/components/keyboard/test_init.py +++ b/tests/components/keyboard/test_init.py @@ -13,9 +13,7 @@ async def test_repair_issue_is_created( issue_registry: ir.IssueRegistry, ) -> None: """Test repair issue is created.""" - from homeassistant.components.keyboard import ( # pylint:disable=import-outside-toplevel - DOMAIN, - ) + from homeassistant.components.keyboard import DOMAIN # noqa: PLC0415 assert await async_setup_component( hass, diff --git a/tests/components/lirc/test_init.py b/tests/components/lirc/test_init.py index 6a0747143df..7cc430d8dd0 100644 --- a/tests/components/lirc/test_init.py +++ b/tests/components/lirc/test_init.py @@ -13,9 +13,7 @@ async def test_repair_issue_is_created( issue_registry: ir.IssueRegistry, ) -> None: """Test repair issue is created.""" - from homeassistant.components.lirc import ( # pylint: disable=import-outside-toplevel - DOMAIN, - ) + from homeassistant.components.lirc import DOMAIN # noqa: PLC0415 assert await async_setup_component( hass, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index af9975de1ea..f789d7f3be1 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -683,11 +683,9 @@ async def test_receiving_message_with_non_utf8_topic_gets_logged( # Local import to avoid processing MQTT modules when running a testcase # which does not use MQTT. - # pylint: disable-next=import-outside-toplevel - from paho.mqtt.client import MQTTMessage + from paho.mqtt.client import MQTTMessage # noqa: PLC0415 - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.mqtt.models import MqttData + from homeassistant.components.mqtt.models import MqttData # noqa: PLC0415 msg = MQTTMessage(topic=b"tasmota/discovery/18FE34E0B760\xcc\x02") msg.payload = b"Payload" @@ -1001,10 +999,9 @@ async def test_dump_service( async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) await hass.async_block_till_done() - writes = mopen.return_value.write.mock_calls - assert len(writes) == 2 - assert writes[0][1][0] == "bla/1,test1\n" - assert writes[1][1][0] == "bla/2,test2\n" + writes = mopen.return_value.writelines.mock_calls + assert len(writes) == 1 + assert writes[0][1][0] == ["bla/1,test1\n", "bla/2,test2\n"] async def test_mqtt_ws_remove_discovered_device( diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py index 47b65772a24..9357163f72a 100644 --- a/tests/components/nibe_heatpump/conftest.py +++ b/tests/components/nibe_heatpump/conftest.py @@ -55,8 +55,7 @@ async def fixture_mock_connection(mock_connection_construct): @pytest.fixture(name="coils") async def fixture_coils(mock_connection: MockConnection): """Return a dict with coil data.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.nibe_heatpump import HeatPump + from homeassistant.components.nibe_heatpump import HeatPump # noqa: PLC0415 get_coils_original = HeatPump.get_coils get_coil_by_address_original = HeatPump.get_coil_by_address diff --git a/tests/components/sms/test_init.py b/tests/components/sms/test_init.py index 68c57ba7f55..05448ce0f57 100644 --- a/tests/components/sms/test_init.py +++ b/tests/components/sms/test_init.py @@ -22,7 +22,7 @@ async def test_repair_issue_is_created( issue_registry: ir.IssueRegistry, ) -> None: """Test repair issue is created.""" - from homeassistant.components.sms import ( # pylint: disable=import-outside-toplevel + from homeassistant.components.sms import ( # noqa: PLC0415 DEPRECATED_ISSUE_ID, DOMAIN, ) diff --git a/tests/conftest.py b/tests/conftest.py index 8b5c5e26c36..ef31eee4004 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -201,8 +201,7 @@ def pytest_runtest_setup() -> None: # Setup HAFakeDatetime converter for pymysql try: - # pylint: disable-next=import-outside-toplevel - import MySQLdb.converters as MySQLdb_converters + import MySQLdb.converters as MySQLdb_converters # noqa: PLC0415 except ImportError: pass else: @@ -1036,7 +1035,7 @@ async def _mqtt_mock_entry( """Fixture to mock a delayed setup of the MQTT config entry.""" # Local import to avoid processing MQTT modules when running a testcase # which does not use MQTT. - from homeassistant.components import mqtt # pylint: disable=import-outside-toplevel + from homeassistant.components import mqtt # noqa: PLC0415 if mqtt_config_entry_data is None: mqtt_config_entry_data = {mqtt.CONF_BROKER: "mock-broker"} @@ -1317,7 +1316,7 @@ def disable_mock_zeroconf_resolver( @pytest.fixture def mock_zeroconf() -> Generator[MagicMock]: """Mock zeroconf.""" - from zeroconf import DNSCache # pylint: disable=import-outside-toplevel + from zeroconf import DNSCache # noqa: PLC0415 with ( patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, @@ -1337,10 +1336,8 @@ def mock_zeroconf() -> Generator[MagicMock]: @pytest.fixture def mock_async_zeroconf(mock_zeroconf: MagicMock) -> Generator[MagicMock]: """Mock AsyncZeroconf.""" - from zeroconf import DNSCache, Zeroconf # pylint: disable=import-outside-toplevel - from zeroconf.asyncio import ( # pylint: disable=import-outside-toplevel - AsyncZeroconf, - ) + from zeroconf import DNSCache, Zeroconf # noqa: PLC0415 + from zeroconf.asyncio import AsyncZeroconf # noqa: PLC0415 with patch( "homeassistant.components.zeroconf.HaAsyncZeroconf", spec=AsyncZeroconf @@ -1496,15 +1493,13 @@ def recorder_db_url( tmp_path = tmp_path_factory.mktemp("recorder") db_url = "sqlite:///" + str(tmp_path / "pytest.db") elif db_url.startswith("mysql://"): - # pylint: disable-next=import-outside-toplevel - import sqlalchemy_utils + import sqlalchemy_utils # noqa: PLC0415 charset = "utf8mb4' COLLATE = 'utf8mb4_unicode_ci" assert not sqlalchemy_utils.database_exists(db_url) sqlalchemy_utils.create_database(db_url, encoding=charset) elif db_url.startswith("postgresql://"): - # pylint: disable-next=import-outside-toplevel - import sqlalchemy_utils + import sqlalchemy_utils # noqa: PLC0415 assert not sqlalchemy_utils.database_exists(db_url) sqlalchemy_utils.create_database(db_url, encoding="utf8") @@ -1512,8 +1507,7 @@ def recorder_db_url( if db_url == "sqlite://" and persistent_database: rmtree(tmp_path, ignore_errors=True) elif db_url.startswith("mysql://"): - # pylint: disable-next=import-outside-toplevel - import sqlalchemy as sa + import sqlalchemy as sa # noqa: PLC0415 made_url = sa.make_url(db_url) db = made_url.database @@ -1544,8 +1538,7 @@ async def _async_init_recorder_component( wait_setup: bool, ) -> None: """Initialize the recorder asynchronously.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder + from homeassistant.components import recorder # noqa: PLC0415 config = dict(add_config) if add_config else {} if recorder.CONF_DB_URL not in config: @@ -1596,21 +1589,16 @@ async def async_test_recorder( enable_migrate_event_ids: bool, ) -> AsyncGenerator[RecorderInstanceContextManager]: """Yield context manager to setup recorder instance.""" - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import recorder + from homeassistant.components import recorder # noqa: PLC0415 + from homeassistant.components.recorder import migration # noqa: PLC0415 - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.recorder import migration - - # pylint: disable-next=import-outside-toplevel - from .components.recorder.common import async_recorder_block_till_done - - # pylint: disable-next=import-outside-toplevel - from .patch_recorder import real_session_scope + from .components.recorder.common import ( # noqa: PLC0415 + async_recorder_block_till_done, + ) + from .patch_recorder import real_session_scope # noqa: PLC0415 if TYPE_CHECKING: - # pylint: disable-next=import-outside-toplevel - from sqlalchemy.orm.session import Session + from sqlalchemy.orm.session import Session # noqa: PLC0415 @contextmanager def debug_session_scope( @@ -1857,8 +1845,7 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]: # Late imports to avoid loading bleak unless we need it - # pylint: disable-next=import-outside-toplevel - from habluetooth import scanner as bluetooth_scanner + from habluetooth import scanner as bluetooth_scanner # noqa: PLC0415 # We need to drop the stop method from the object since we patched # out start and this fixture will expire before the stop method is called @@ -1878,13 +1865,9 @@ def mock_bleak_scanner_start() -> Generator[MagicMock]: @pytest.fixture def hassio_env(supervisor_is_connected: AsyncMock) -> Generator[None]: """Fixture to inject hassio env.""" - from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel - HassioAPIError, - ) + from homeassistant.components.hassio import HassioAPIError # noqa: PLC0415 - from .components.hassio import ( # pylint: disable=import-outside-toplevel - SUPERVISOR_TOKEN, - ) + from .components.hassio import SUPERVISOR_TOKEN # noqa: PLC0415 with ( patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), @@ -1906,9 +1889,7 @@ async def hassio_stubs( supervisor_client: AsyncMock, ) -> RefreshToken: """Create mock hassio http client.""" - from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel - HassioAPIError, - ) + from homeassistant.components.hassio import HassioAPIError # noqa: PLC0415 with ( patch( diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index e99db76dcbc..54ebfaf953e 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -39,8 +39,9 @@ async def test_get_integration_logger( @pytest.mark.usefixtures("enable_custom_integrations", "hass") async def test_extract_frame_resolve_module() -> None: """Test extracting the current frame from integration context.""" - # pylint: disable-next=import-outside-toplevel - from custom_components.test_integration_frame import call_get_integration_frame + from custom_components.test_integration_frame import ( # noqa: PLC0415 + call_get_integration_frame, + ) integration_frame = call_get_integration_frame() @@ -56,8 +57,9 @@ async def test_extract_frame_resolve_module() -> None: @pytest.mark.usefixtures("enable_custom_integrations", "hass") async def test_get_integration_logger_resolve_module() -> None: """Test getting the logger from integration context.""" - # pylint: disable-next=import-outside-toplevel - from custom_components.test_integration_frame import call_get_integration_logger + from custom_components.test_integration_frame import ( # noqa: PLC0415 + call_get_integration_logger, + ) logger = call_get_integration_logger(__name__) diff --git a/tests/test_loader.py b/tests/test_loader.py index 16515cbd4e6..2d5ad76aa8a 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -134,8 +134,7 @@ async def test_custom_component_name(hass: HomeAssistant) -> None: assert platform.__package__ == "custom_components.test" # Test custom components is mounted - # pylint: disable-next=import-outside-toplevel - from custom_components.test_package import TEST + from custom_components.test_package import TEST # noqa: PLC0415 assert TEST == 5 @@ -1295,12 +1294,11 @@ async def test_config_folder_not_in_path() -> None: # Verify that we are unable to import this file from top level with pytest.raises(ImportError): - # pylint: disable-next=import-outside-toplevel - import check_config_not_in_path # noqa: F401 + import check_config_not_in_path # noqa: F401, PLC0415 # Verify that we are able to load the file with absolute path - # pylint: disable-next=import-outside-toplevel,hass-relative-import - import tests.testing_config.check_config_not_in_path # noqa: F401 + # pylint: disable-next=hass-relative-import + import tests.testing_config.check_config_not_in_path # noqa: F401, PLC0415 async def test_async_get_component_preloads_config_and_config_flow( diff --git a/tests/test_main.py b/tests/test_main.py index d32ca59a846..acb0146545e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -36,7 +36,7 @@ def test_validate_python(mock_exit) -> None: with patch( "sys.version_info", new_callable=PropertyMock( - return_value=(REQUIRED_PYTHON_VER[0] - 1,) + REQUIRED_PYTHON_VER[1:] + return_value=(REQUIRED_PYTHON_VER[0] - 1, *REQUIRED_PYTHON_VER[1:]) ), ): main.validate_python() @@ -55,7 +55,7 @@ def test_validate_python(mock_exit) -> None: with patch( "sys.version_info", new_callable=PropertyMock( - return_value=(REQUIRED_PYTHON_VER[:2]) + (REQUIRED_PYTHON_VER[2] + 1,) + return_value=(*REQUIRED_PYTHON_VER[:2], REQUIRED_PYTHON_VER[2] + 1) ), ): main.validate_python() From 341d9f15f025185bd912f5c25ba12231c62dad96 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 19 Jun 2025 16:50:14 -0500 Subject: [PATCH 25/69] Add ask_question action to Assist satellite (#145233) * Add get_response to Assist satellite and ESPHome * Rename get_response to ask_question * Add possible answers to questions * Add wildcard support and entity test * Add ESPHome test * Refactor to remove async_ask_question * Use single entity_id instead of target * Fix error message * Remove ESPHome test * Clean up * Revert fix --- .../components/assist_satellite/__init__.py | 96 ++++++++++- .../components/assist_satellite/entity.py | 161 +++++++++++++++++- .../components/assist_satellite/icons.json | 3 + .../components/assist_satellite/manifest.json | 3 +- .../components/assist_satellite/services.yaml | 32 ++++ .../components/assist_satellite/strings.json | 30 ++++ requirements_all.txt | 1 + requirements_test_all.txt | 1 + .../assist_satellite/test_entity.py | 123 +++++++++++++ 9 files changed, 447 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/assist_satellite/__init__.py b/homeassistant/components/assist_satellite/__init__.py index 3338f223bc9..f1f38f343f9 100644 --- a/homeassistant/components/assist_satellite/__init__.py +++ b/homeassistant/components/assist_satellite/__init__.py @@ -1,13 +1,23 @@ """Base class for assist satellite entities.""" +from dataclasses import asdict import logging from pathlib import Path +from typing import Any +from hassil.util import ( + PUNCTUATION_END, + PUNCTUATION_END_WORD, + PUNCTUATION_START, + PUNCTUATION_START_WORD, +) import voluptuous as vol from homeassistant.components.http import StaticPathConfig from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -23,6 +33,7 @@ from .const import ( ) from .entity import ( AssistSatelliteAnnouncement, + AssistSatelliteAnswer, AssistSatelliteConfiguration, AssistSatelliteEntity, AssistSatelliteEntityDescription, @@ -34,6 +45,7 @@ from .websocket_api import async_register_websocket_api __all__ = [ "DOMAIN", "AssistSatelliteAnnouncement", + "AssistSatelliteAnswer", "AssistSatelliteConfiguration", "AssistSatelliteEntity", "AssistSatelliteEntityDescription", @@ -86,6 +98,62 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_internal_start_conversation", [AssistSatelliteEntityFeature.START_CONVERSATION], ) + + async def handle_ask_question(call: ServiceCall) -> dict[str, Any]: + """Handle a Show View service call.""" + satellite_entity_id: str = call.data[ATTR_ENTITY_ID] + satellite_entity: AssistSatelliteEntity | None = component.get_entity( + satellite_entity_id + ) + if satellite_entity is None: + raise HomeAssistantError( + f"Invalid Assist satellite entity id: {satellite_entity_id}" + ) + + ask_question_args = { + "question": call.data.get("question"), + "question_media_id": call.data.get("question_media_id"), + "preannounce": call.data.get("preannounce", False), + "answers": call.data.get("answers"), + } + + if preannounce_media_id := call.data.get("preannounce_media_id"): + ask_question_args["preannounce_media_id"] = preannounce_media_id + + answer = await satellite_entity.async_internal_ask_question(**ask_question_args) + + if answer is None: + raise HomeAssistantError("No answer from satellite") + + return asdict(answer) + + hass.services.async_register( + domain=DOMAIN, + service="ask_question", + service_func=handle_ask_question, + schema=vol.All( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional("question"): str, + vol.Optional("question_media_id"): str, + vol.Optional("preannounce"): bool, + vol.Optional("preannounce_media_id"): str, + vol.Optional("answers"): [ + { + vol.Required("id"): str, + vol.Required("sentences"): vol.All( + cv.ensure_list, + [cv.string], + has_one_non_empty_item, + has_no_punctuation, + ), + } + ], + }, + cv.has_at_least_one_key("question", "question_media_id"), + ), + supports_response=SupportsResponse.ONLY, + ) hass.data[CONNECTION_TEST_DATA] = {} async_register_websocket_api(hass) hass.http.register_view(ConnectionTestView()) @@ -110,3 +178,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.data[DATA_COMPONENT].async_unload_entry(entry) + + +def has_no_punctuation(value: list[str]) -> list[str]: + """Validate result does not contain punctuation.""" + for sentence in value: + if ( + PUNCTUATION_START.search(sentence) + or PUNCTUATION_END.search(sentence) + or PUNCTUATION_START_WORD.search(sentence) + or PUNCTUATION_END_WORD.search(sentence) + ): + raise vol.Invalid("sentence should not contain punctuation") + + return value + + +def has_one_non_empty_item(value: list[str]) -> list[str]: + """Validate result has at least one item.""" + if len(value) < 1: + raise vol.Invalid("at least one sentence is required") + + for sentence in value: + if not sentence: + raise vol.Invalid("sentences cannot be empty") + + return value diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index dc20c7650d7..d32bad2c824 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -4,12 +4,16 @@ from abc import abstractmethod import asyncio from collections.abc import AsyncIterable import contextlib -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import StrEnum import logging import time from typing import Any, Literal, final +from hassil import Intents, recognize +from hassil.expression import Expression, ListReference, Sequence +from hassil.intents import WildcardSlotList + from homeassistant.components import conversation, media_source, stt, tts from homeassistant.components.assist_pipeline import ( OPTION_PREFERRED, @@ -105,6 +109,20 @@ class AssistSatelliteAnnouncement: """Media ID to be played before announcement.""" +@dataclass +class AssistSatelliteAnswer: + """Answer to a question.""" + + id: str | None + """Matched answer id or None if no answer was matched.""" + + sentence: str + """Raw sentence text from user response.""" + + slots: dict[str, Any] = field(default_factory=dict) + """Matched slots from answer.""" + + class AssistSatelliteEntity(entity.Entity): """Entity encapsulating the state and functionality of an Assist satellite.""" @@ -120,8 +138,10 @@ class AssistSatelliteEntity(entity.Entity): _is_announcing = False _extra_system_prompt: str | None = None _wake_word_intercept_future: asyncio.Future[str | None] | None = None + _stt_intercept_future: asyncio.Future[str | None] | None = None _attr_tts_options: dict[str, Any] | None = None _pipeline_task: asyncio.Task | None = None + _ask_question_future: asyncio.Future[str | None] | None = None __assist_satellite_state = AssistSatelliteState.IDLE @@ -309,6 +329,112 @@ class AssistSatelliteEntity(entity.Entity): """Start a conversation from the satellite.""" raise NotImplementedError + async def async_internal_ask_question( + self, + question: str | None = None, + question_media_id: str | None = None, + preannounce: bool = True, + preannounce_media_id: str = PREANNOUNCE_URL, + answers: list[dict[str, Any]] | None = None, + ) -> AssistSatelliteAnswer | None: + """Ask a question and get a user's response from the satellite. + + If question_media_id is not provided, question is synthesized to audio + with the selected pipeline. + + If question_media_id is provided, it is played directly. It is possible + to omit the message and the satellite will not show any text. + + If preannounce is True, a sound is played before the start message or media. + If preannounce_media_id is provided, it overrides the default sound. + + Calls async_start_conversation. + """ + await self._cancel_running_pipeline() + + if question is None: + question = "" + + announcement = await self._resolve_announcement_media_id( + question, + question_media_id, + preannounce_media_id=preannounce_media_id if preannounce else None, + ) + + if self._is_announcing: + raise SatelliteBusyError + + self._is_announcing = True + self._set_state(AssistSatelliteState.RESPONDING) + self._ask_question_future = asyncio.Future() + + try: + # Wait for announcement to finish + await self.async_start_conversation(announcement) + + # Wait for response text + response_text = await self._ask_question_future + if response_text is None: + raise HomeAssistantError("No answer from question") + + if not answers: + return AssistSatelliteAnswer(id=None, sentence=response_text) + + return self._question_response_to_answer(response_text, answers) + finally: + self._is_announcing = False + self._set_state(AssistSatelliteState.IDLE) + self._ask_question_future = None + + def _question_response_to_answer( + self, response_text: str, answers: list[dict[str, Any]] + ) -> AssistSatelliteAnswer: + """Match text to a pre-defined set of answers.""" + + # Build intents and match + intents = Intents.from_dict( + { + "language": self.hass.config.language, + "intents": { + "QuestionIntent": { + "data": [ + { + "sentences": answer["sentences"], + "metadata": {"answer_id": answer["id"]}, + } + for answer in answers + ] + } + }, + } + ) + + # Assume slot list references are wildcards + wildcard_names: set[str] = set() + for intent in intents.intents.values(): + for intent_data in intent.data: + for sentence in intent_data.sentences: + _collect_list_references(sentence, wildcard_names) + + for wildcard_name in wildcard_names: + intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name) + + # Match response text + result = recognize(response_text, intents) + if result is None: + # No match + return AssistSatelliteAnswer(id=None, sentence=response_text) + + assert result.intent_metadata + return AssistSatelliteAnswer( + id=result.intent_metadata["answer_id"], + sentence=response_text, + slots={ + entity_name: entity.value + for entity_name, entity in result.entities.items() + }, + ) + async def async_accept_pipeline_from_satellite( self, audio_stream: AsyncIterable[bytes], @@ -351,6 +477,11 @@ class AssistSatelliteEntity(entity.Entity): self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END)) return + if (self._ask_question_future is not None) and ( + start_stage == PipelineStage.STT + ): + end_stage = PipelineStage.STT + device_id = self.registry_entry.device_id if self.registry_entry else None # Refresh context if necessary @@ -433,6 +564,16 @@ class AssistSatelliteEntity(entity.Entity): self._set_state(AssistSatelliteState.IDLE) elif event.type is PipelineEventType.STT_START: self._set_state(AssistSatelliteState.LISTENING) + elif event.type is PipelineEventType.STT_END: + # Intercepting text for ask question + if ( + (self._ask_question_future is not None) + and (not self._ask_question_future.done()) + and event.data + ): + self._ask_question_future.set_result( + event.data.get("stt_output", {}).get("text") + ) elif event.type is PipelineEventType.INTENT_START: self._set_state(AssistSatelliteState.PROCESSING) elif event.type is PipelineEventType.TTS_START: @@ -443,6 +584,12 @@ class AssistSatelliteEntity(entity.Entity): if not self._run_has_tts: self._set_state(AssistSatelliteState.IDLE) + if (self._ask_question_future is not None) and ( + not self._ask_question_future.done() + ): + # No text for ask question + self._ask_question_future.set_result(None) + self.on_pipeline_event(event) @callback @@ -577,3 +724,15 @@ class AssistSatelliteEntity(entity.Entity): media_id_source=media_id_source, preannounce_media_id=preannounce_media_id, ) + + +def _collect_list_references(expression: Expression, list_names: set[str]) -> None: + """Collect list reference names recursively.""" + if isinstance(expression, Sequence): + seq: Sequence = expression + for item in seq.items: + _collect_list_references(item, list_names) + elif isinstance(expression, ListReference): + # {list} + list_ref: ListReference = expression + list_names.add(list_ref.slot_name) diff --git a/homeassistant/components/assist_satellite/icons.json b/homeassistant/components/assist_satellite/icons.json index 1ed29541621..fc2589ea506 100644 --- a/homeassistant/components/assist_satellite/icons.json +++ b/homeassistant/components/assist_satellite/icons.json @@ -10,6 +10,9 @@ }, "start_conversation": { "service": "mdi:forum" + }, + "ask_question": { + "service": "mdi:microphone-question" } } } diff --git a/homeassistant/components/assist_satellite/manifest.json b/homeassistant/components/assist_satellite/manifest.json index 68a3ceafd4f..97362f157e4 100644 --- a/homeassistant/components/assist_satellite/manifest.json +++ b/homeassistant/components/assist_satellite/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["assist_pipeline", "http", "stt", "tts"], "documentation": "https://www.home-assistant.io/integrations/assist_satellite", "integration_type": "entity", - "quality_scale": "internal" + "quality_scale": "internal", + "requirements": ["hassil==2.2.3"] } diff --git a/homeassistant/components/assist_satellite/services.yaml b/homeassistant/components/assist_satellite/services.yaml index d88710c4c4e..c5484e22dad 100644 --- a/homeassistant/components/assist_satellite/services.yaml +++ b/homeassistant/components/assist_satellite/services.yaml @@ -54,3 +54,35 @@ start_conversation: required: false selector: text: +ask_question: + fields: + entity_id: + required: true + selector: + entity: + domain: assist_satellite + supported_features: + - assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION + question: + required: false + example: "What kind of music would you like to play?" + default: "" + selector: + text: + question_media_id: + required: false + selector: + text: + preannounce: + required: false + default: true + selector: + boolean: + preannounce_media_id: + required: false + selector: + text: + answers: + required: false + selector: + object: diff --git a/homeassistant/components/assist_satellite/strings.json b/homeassistant/components/assist_satellite/strings.json index b69711c7106..e0bf2bcfb94 100644 --- a/homeassistant/components/assist_satellite/strings.json +++ b/homeassistant/components/assist_satellite/strings.json @@ -59,6 +59,36 @@ "description": "Custom media ID to play before the start message or media." } } + }, + "ask_question": { + "name": "Ask question", + "description": "Asks a question and gets the user's response.", + "fields": { + "entity_id": { + "name": "Entity", + "description": "Assist satellite entity to ask the question on." + }, + "question": { + "name": "Question", + "description": "The question to ask." + }, + "question_media_id": { + "name": "Question media ID", + "description": "The media ID of the question to use instead of text-to-speech." + }, + "preannounce": { + "name": "Preannounce", + "description": "Play a sound before the start message or media." + }, + "preannounce_media_id": { + "name": "Preannounce media ID", + "description": "Custom media ID to play before the start message or media." + }, + "answers": { + "name": "Answers", + "description": "Possible answers to the question." + } + } } } } diff --git a/requirements_all.txt b/requirements_all.txt index a3f0c833d2f..cf683a09e67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,6 +1129,7 @@ hass-nabucasa==0.102.0 # homeassistant.components.splunk hass-splunk==0.1.1 +# homeassistant.components.assist_satellite # homeassistant.components.conversation hassil==2.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a27b9f5d199..3f513185014 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -984,6 +984,7 @@ habluetooth==3.49.0 # homeassistant.components.cloud hass-nabucasa==0.102.0 +# homeassistant.components.assist_satellite # homeassistant.components.conversation hassil==2.2.3 diff --git a/tests/components/assist_satellite/test_entity.py b/tests/components/assist_satellite/test_entity.py index 8050b23f5ff..3473b23bedd 100644 --- a/tests/components/assist_satellite/test_entity.py +++ b/tests/components/assist_satellite/test_entity.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import Generator +from dataclasses import asdict from unittest.mock import Mock, patch import pytest @@ -20,6 +21,7 @@ from homeassistant.components.assist_pipeline import ( ) from homeassistant.components.assist_satellite import ( AssistSatelliteAnnouncement, + AssistSatelliteAnswer, SatelliteBusyError, ) from homeassistant.components.assist_satellite.const import PREANNOUNCE_URL @@ -708,6 +710,127 @@ async def test_start_conversation_default_preannounce( ) +@pytest.mark.parametrize( + ("service_data", "response_text", "expected_answer"), + [ + ( + {"preannounce": False}, + "jazz", + AssistSatelliteAnswer(id=None, sentence="jazz"), + ), + ( + { + "answers": [ + {"id": "jazz", "sentences": ["[some] jazz [please]"]}, + {"id": "rock", "sentences": ["[some] rock [please]"]}, + ], + "preannounce": False, + }, + "Some Rock, please.", + AssistSatelliteAnswer(id="rock", sentence="Some Rock, please."), + ), + ( + { + "answers": [ + { + "id": "genre", + "sentences": ["genre {genre} [please]"], + }, + { + "id": "artist", + "sentences": ["artist {artist} [please]"], + }, + ], + "preannounce": False, + }, + "artist Pink Floyd", + AssistSatelliteAnswer( + id="artist", + sentence="artist Pink Floyd", + slots={"artist": "Pink Floyd"}, + ), + ), + ], +) +async def test_ask_question( + hass: HomeAssistant, + init_components: ConfigEntry, + entity: MockAssistSatellite, + service_data: dict, + response_text: str, + expected_answer: AssistSatelliteAnswer, +) -> None: + """Test asking a question on a device and matching an answer.""" + entity_id = "assist_satellite.test_entity" + question_text = "What kind of music would you like to listen to?" + + await async_update_pipeline( + hass, async_get_pipeline(hass), stt_engine="test-stt-engine", stt_language="en" + ) + + async def speech_to_text(self, *args, **kwargs): + self.process_event( + PipelineEvent( + PipelineEventType.STT_END, {"stt_output": {"text": response_text}} + ) + ) + + return response_text + + original_start_conversation = entity.async_start_conversation + + async def async_start_conversation(start_announcement): + # Verify state change + assert entity.state == AssistSatelliteState.RESPONDING + await original_start_conversation(start_announcement) + + audio_stream = object() + with ( + patch( + "homeassistant.components.assist_pipeline.pipeline.PipelineRun.prepare_speech_to_text" + ), + patch( + "homeassistant.components.assist_pipeline.pipeline.PipelineRun.speech_to_text", + speech_to_text, + ), + ): + await entity.async_accept_pipeline_from_satellite( + audio_stream, start_stage=PipelineStage.STT + ) + + with ( + patch( + "homeassistant.components.tts.generate_media_source_id", + return_value="media-source://generated", + ), + patch( + "homeassistant.components.tts.async_resolve_engine", + return_value="tts.cloud", + ), + patch( + "homeassistant.components.tts.async_create_stream", + return_value=MockResultStream(hass, "wav", b""), + ), + patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=PlayMedia( + url="https://www.home-assistant.io/resolved.mp3", + mime_type="audio/mp3", + ), + ), + patch.object(entity, "async_start_conversation", new=async_start_conversation), + ): + response = await hass.services.async_call( + "assist_satellite", + "ask_question", + {"entity_id": entity_id, "question": question_text, **service_data}, + blocking=True, + return_response=True, + ) + assert entity.state == AssistSatelliteState.IDLE + assert response == asdict(expected_answer) + + async def test_wake_word_start_keeps_responding( hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite ) -> None: From 11564e3df5931d40f861da058fbba24538e38033 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 20 Jun 2025 07:56:20 +0200 Subject: [PATCH 26/69] Fix Z-Wave device class endpoint discovery (#142171) * Add test fixture and test for Glass 9 shutter * Fix zwave_js device class discovery matcher * Fall back to node device class * Fix test_special_meters modifying node state * Handle value added after node ready --- .../components/zwave_js/discovery.py | 44 +- tests/components/zwave_js/conftest.py | 14 + .../cover_mco_home_glass_9_shutter_state.json | 4988 +++++++++++++++++ .../fixtures/touchwand_glass9_state.json | 3467 ++++++++++++ tests/components/zwave_js/test_discovery.py | 18 + tests/components/zwave_js/test_init.py | 3 +- tests/components/zwave_js/test_sensor.py | 22 + 7 files changed, 8547 insertions(+), 9 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/cover_mco_home_glass_9_shutter_state.json create mode 100644 tests/components/zwave_js/fixtures/touchwand_glass9_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 92233dd2e77..3b541a733cc 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1334,21 +1334,49 @@ def async_discover_single_value( continue # check device_class_generic + # If the value has an endpoint but it is missing on the node + # we can't match the endpoint device class to the schema device class. + # This could happen if the value is discovered after the node is ready. if schema.device_class_generic and ( - not value.node.device_class - or not any( - value.node.device_class.generic.label == val - for val in schema.device_class_generic + ( + (endpoint := value.endpoint) is None + or (node_endpoint := value.node.endpoints.get(endpoint)) is None + or (device_class := node_endpoint.device_class) is None + or not any( + device_class.generic.label == val + for val in schema.device_class_generic + ) + ) + and ( + (device_class := value.node.device_class) is None + or not any( + device_class.generic.label == val + for val in schema.device_class_generic + ) ) ): continue # check device_class_specific + # If the value has an endpoint but it is missing on the node + # we can't match the endpoint device class to the schema device class. + # This could happen if the value is discovered after the node is ready. if schema.device_class_specific and ( - not value.node.device_class - or not any( - value.node.device_class.specific.label == val - for val in schema.device_class_specific + ( + (endpoint := value.endpoint) is None + or (node_endpoint := value.node.endpoints.get(endpoint)) is None + or (device_class := node_endpoint.device_class) is None + or not any( + device_class.specific.label == val + for val in schema.device_class_specific + ) + ) + and ( + (device_class := value.node.device_class) is None + or not any( + device_class.specific.label == val + for val in schema.device_class_specific + ) ) ): continue diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 25f40e4418d..138bcd63ede 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -301,6 +301,12 @@ def shelly_europe_ltd_qnsh_001p10_state_fixture() -> dict[str, Any]: return load_json_object_fixture("shelly_europe_ltd_qnsh_001p10_state.json", DOMAIN) +@pytest.fixture(name="touchwand_glass9_state", scope="package") +def touchwand_glass9_state_fixture() -> dict[str, Any]: + """Load the Touchwand Glass 9 shutter node state fixture data.""" + return load_json_object_fixture("touchwand_glass9_state.json", DOMAIN) + + @pytest.fixture(name="merten_507801_state", scope="package") def merten_507801_state_fixture() -> dict[str, Any]: """Load the Merten 507801 Shutter node state fixture data.""" @@ -1040,6 +1046,14 @@ def shelly_qnsh_001P10_cover_shutter_fixture( return node +@pytest.fixture(name="touchwand_glass9") +def touchwand_glass9_fixture(client, touchwand_glass9_state) -> Node: + """Mock a Touchwand glass9 node.""" + node = Node(client, copy.deepcopy(touchwand_glass9_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="merten_507801") def merten_507801_cover_fixture(client, merten_507801_state) -> Node: """Mock a Merten 507801 Shutter node.""" diff --git a/tests/components/zwave_js/fixtures/cover_mco_home_glass_9_shutter_state.json b/tests/components/zwave_js/fixtures/cover_mco_home_glass_9_shutter_state.json new file mode 100644 index 00000000000..13b5d0495f9 --- /dev/null +++ b/tests/components/zwave_js/fixtures/cover_mco_home_glass_9_shutter_state.json @@ -0,0 +1,4988 @@ +{ + "home_assistant": { + "installation_type": "Home Assistant Container", + "version": "2024.7.4", + "dev": false, + "hassio": false, + "virtualenv": false, + "python_version": "3.12.4", + "docker": true, + "arch": "armv7l", + "timezone": "Asia/Jerusalem", + "os_name": "Linux", + "os_version": "5.4.142-g5227ff0e2a5c-dirty", + "run_as_root": true + }, + "custom_components": { + "oref_alert": { + "documentation": "https://github.com/amitfin/oref_alert", + "version": "v2.11.3", + "requirements": ["haversine==2.8.1", "shapely==2.0.4"] + }, + "scheduler": { + "documentation": "https://github.com/nielsfaber/scheduler-component", + "version": "v0.0.0", + "requirements": [] + }, + "hebcal": { + "documentation": "https://github.com/rt400/Jewish-Sabbaths-Holidays", + "version": "2.4.0", + "requirements": [] + }, + "hacs": { + "documentation": "https://hacs.xyz/docs/configuration/start", + "version": "1.34.0", + "requirements": ["aiogithubapi>=22.10.1"] + } + }, + "integration_manifest": { + "domain": "zwave_js", + "name": "Z-Wave", + "codeowners": ["home-assistant/z-wave"], + "config_flow": true, + "dependencies": ["http", "repairs", "usb", "websocket_api"], + "documentation": "https://www.home-assistant.io/integrations/zwave_js", + "integration_type": "hub", + "iot_class": "local_push", + "loggers": ["zwave_js_server"], + "quality_scale": "platinum", + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.57.0"], + "usb": [ + { + "vid": "0658", + "pid": "0200", + "known_devices": ["Aeotec Z-Stick Gen5+", "Z-WaveMe UZB"] + }, + { + "vid": "10C4", + "pid": "8A2A", + "description": "*z-wave*", + "known_devices": ["Nortek HUSBZB-1"] + } + ], + "zeroconf": ["_zwave-js-server._tcp.local."], + "is_built_in": true + }, + "setup_times": { + "null": { + "setup": 0.06139277799957199 + }, + "01J4GRKFXZDKNDWCNE0ZWKH65M": { + "config_entry_setup": 0.22992777000035858, + "config_entry_platform_setup": 0.12791325299986056, + "wait_base_component": -0.009490847998677054 + } + }, + "data": { + "versionInfo": { + "driverVersion": "13.0.2", + "serverVersion": "1.37.0", + "minSchemaVersion": 0, + "maxSchemaVersion": 37 + }, + "entities": [ + { + "domain": "sensor", + "entity_id": "sensor.gp9_air_temperature", + "original_name": "Air temperature", + "original_device_class": "temperature", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "\u00b0C", + "value_id": "46-49-1-Air temperature", + "primary_value": { + "command_class": 49, + "command_class_name": "Multilevel Sensor", + "endpoint": 1, + "property": "Air temperature", + "property_name": "Air temperature", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_kwh", + "original_name": "Electric Consumption [kWh]", + "original_device_class": "energy", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "kWh", + "value_id": "46-50-8-value-65537", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 8, + "property": "value", + "property_name": "value", + "property_key": 65537, + "property_key_name": "Electric_kWh_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_w", + "original_name": "Electric Consumption [W]", + "original_device_class": "power", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "W", + "value_id": "46-50-8-value-66049", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 8, + "property": "value", + "property_name": "value", + "property_key": 66049, + "property_key_name": "Electric_W_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_v", + "original_name": "Electric Consumption [V]", + "original_device_class": "voltage", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "V", + "value_id": "46-50-8-value-66561", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 8, + "property": "value", + "property_name": "value", + "property_key": 66561, + "property_key_name": "Electric_V_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_a", + "original_name": "Electric Consumption [A]", + "original_device_class": "current", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "A", + "value_id": "46-50-8-value-66817", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 8, + "property": "value", + "property_name": "value", + "property_key": 66817, + "property_key_name": "Electric_A_Consumed" + } + }, + { + "domain": "button", + "entity_id": "button.gp9_reset_accumulated_values", + "original_name": "Reset accumulated values", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-50-8-reset", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 8, + "property": "reset", + "property_name": "reset", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_alarmtype", + "original_name": "alarmType", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-8-alarmType", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 8, + "property": "alarmType", + "property_name": "alarmType", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_alarmlevel", + "original_name": "alarmLevel", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-8-alarmLevel", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 8, + "property": "alarmLevel", + "property_name": "alarmLevel", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_power_management_over_current_status", + "original_name": "Power Management Over-current status", + "original_device_class": "enum", + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-8-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 8, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status" + } + }, + { + "domain": "button", + "entity_id": "button.gp9_idle_power_management_over_current_status", + "original_name": "Idle Power Management Over-current status", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-8-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 8, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_kwh_9", + "original_name": "Electric Consumption [kWh] (9)", + "original_device_class": "energy", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "kWh", + "value_id": "46-50-9-value-65537", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 9, + "property": "value", + "property_name": "value", + "property_key": 65537, + "property_key_name": "Electric_kWh_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_w_9", + "original_name": "Electric Consumption [W] (9)", + "original_device_class": "power", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "W", + "value_id": "46-50-9-value-66049", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 9, + "property": "value", + "property_name": "value", + "property_key": 66049, + "property_key_name": "Electric_W_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_v_9", + "original_name": "Electric Consumption [V] (9)", + "original_device_class": "voltage", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "V", + "value_id": "46-50-9-value-66561", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 9, + "property": "value", + "property_name": "value", + "property_key": 66561, + "property_key_name": "Electric_V_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_a_9", + "original_name": "Electric Consumption [A] (9)", + "original_device_class": "current", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "A", + "value_id": "46-50-9-value-66817", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 9, + "property": "value", + "property_name": "value", + "property_key": 66817, + "property_key_name": "Electric_A_Consumed" + } + }, + { + "domain": "button", + "entity_id": "button.gp9_reset_accumulated_values_9", + "original_name": "Reset accumulated values (9)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-50-9-reset", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 9, + "property": "reset", + "property_name": "reset", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_alarmtype_9", + "original_name": "alarmType (9)", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-9-alarmType", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 9, + "property": "alarmType", + "property_name": "alarmType", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_alarmlevel_9", + "original_name": "alarmLevel (9)", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-9-alarmLevel", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 9, + "property": "alarmLevel", + "property_name": "alarmLevel", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_power_management_over_current_status_9", + "original_name": "Power Management Over-current status (9)", + "original_device_class": "enum", + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-9-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 9, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status" + } + }, + { + "domain": "button", + "entity_id": "button.gp9_idle_power_management_over_current_status_9", + "original_name": "Idle Power Management Over-current status (9)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-9-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 9, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_kwh_10", + "original_name": "Electric Consumption [kWh] (10)", + "original_device_class": "energy", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "kWh", + "value_id": "46-50-10-value-65537", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 10, + "property": "value", + "property_name": "value", + "property_key": 65537, + "property_key_name": "Electric_kWh_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_w_10", + "original_name": "Electric Consumption [W] (10)", + "original_device_class": "power", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "W", + "value_id": "46-50-10-value-66049", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 10, + "property": "value", + "property_name": "value", + "property_key": 66049, + "property_key_name": "Electric_W_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_v_10", + "original_name": "Electric Consumption [V] (10)", + "original_device_class": "voltage", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "V", + "value_id": "46-50-10-value-66561", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 10, + "property": "value", + "property_name": "value", + "property_key": 66561, + "property_key_name": "Electric_V_Consumed" + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_consumption_a_10", + "original_name": "Electric Consumption [A] (10)", + "original_device_class": "current", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "A", + "value_id": "46-50-10-value-66817", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 10, + "property": "value", + "property_name": "value", + "property_key": 66817, + "property_key_name": "Electric_A_Consumed" + } + }, + { + "domain": "button", + "entity_id": "button.gp9_reset_accumulated_values_10", + "original_name": "Reset accumulated values (10)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-50-10-reset", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 10, + "property": "reset", + "property_name": "reset", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_alarmtype_10", + "original_name": "alarmType (10)", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-10-alarmType", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 10, + "property": "alarmType", + "property_name": "alarmType", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_alarmlevel_10", + "original_name": "alarmLevel (10)", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-10-alarmLevel", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 10, + "property": "alarmLevel", + "property_name": "alarmLevel", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_power_management_over_current_status_10", + "original_name": "Power Management Over-current status (10)", + "original_device_class": "enum", + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-10-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 10, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status" + } + }, + { + "domain": "button", + "entity_id": "button.gp9_idle_power_management_over_current_status_10", + "original_name": "Idle Power Management Over-current status (10)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-10-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 10, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status" + } + }, + { + "domain": "light", + "entity_id": "light.gp9", + "original_name": "", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 32, + "unit_of_measurement": null, + "value_id": "46-38-8-currentValue", + "primary_value": { + "command_class": 38, + "command_class_name": "Multilevel Switch", + "endpoint": 8, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "light", + "entity_id": "light.gp9_9", + "original_name": "(9)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 32, + "unit_of_measurement": null, + "value_id": "46-38-9-currentValue", + "primary_value": { + "command_class": 38, + "command_class_name": "Multilevel Switch", + "endpoint": 9, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "light", + "entity_id": "light.gp9_10", + "original_name": "(10)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 32, + "unit_of_measurement": null, + "value_id": "46-38-10-currentValue", + "primary_value": { + "command_class": 38, + "command_class_name": "Multilevel Switch", + "endpoint": 10, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id", + "original_name": "Scene ID", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-2-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 2, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_3", + "original_name": "Scene ID (3)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-3-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 3, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_4", + "original_name": "Scene ID (4)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-4-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 4, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_5", + "original_name": "Scene ID (5)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-5-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 5, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_6", + "original_name": "Scene ID (6)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-6-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 6, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_7", + "original_name": "Scene ID (7)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-7-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 7, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_8", + "original_name": "Scene ID (8)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-8-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 8, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_9", + "original_name": "Scene ID (9)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-9-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 9, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_10", + "original_name": "Scene ID (10)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-10-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 10, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_11", + "original_name": "Scene ID (11)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-11-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 11, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_12", + "original_name": "Scene ID (12)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-12-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 12, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "event", + "entity_id": "event.gp9_scene_id_13", + "original_name": "Scene ID (13)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-43-13-sceneId", + "primary_value": { + "command_class": 43, + "command_class_name": "Scene Activation", + "endpoint": 13, + "property": "sceneId", + "property_name": "sceneId", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_3", + "original_name": "(3)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-3-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 3, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_4", + "original_name": "(4)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-4-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 4, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_5", + "original_name": "(5)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-5-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 5, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_6", + "original_name": "(6)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-6-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 6, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_7", + "original_name": "(7)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-7-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 7, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_8", + "original_name": "(8)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-8-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 8, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_9", + "original_name": "(9)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-9-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 9, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_10", + "original_name": "(10)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-10-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 10, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_11", + "original_name": "(11)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-11-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 11, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_12", + "original_name": "(12)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-12-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 12, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "switch", + "entity_id": "switch.gp9_13", + "original_name": "(13)", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-13-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 13, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.gp9_over_current_detected", + "original_name": "Over-current detected", + "original_device_class": "safety", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-8-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 8, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status", + "state_key": 6 + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.gp9_over_current_detected_9", + "original_name": "Over-current detected (9)", + "original_device_class": "safety", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-9-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 9, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status", + "state_key": 6 + } + }, + { + "domain": "binary_sensor", + "entity_id": "binary_sensor.gp9_over_current_detected_10", + "original_name": "Over-current detected (10)", + "original_device_class": "safety", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": "diagnostic", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-113-10-Power Management-Over-current status", + "primary_value": { + "command_class": 113, + "command_class_name": "Notification", + "endpoint": 10, + "property": "Power Management", + "property_name": "Power Management", + "property_key": "Over-current status", + "property_key_name": "Over-current status", + "state_key": 6 + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_w", + "original_name": "Electric [W]", + "original_device_class": "power", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "W", + "value_id": "46-50-9-value-66051", + "primary_value": null + }, + { + "domain": "number", + "entity_id": "number.gp9_param123", + "original_name": "param123", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-112-0-123", + "primary_value": null + }, + { + "domain": "number", + "entity_id": "number.gp9_param120", + "original_name": "param120", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-112-0-120", + "primary_value": null + }, + { + "domain": "number", + "entity_id": "number.gp9_param124", + "original_name": "param124", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-112-0-124", + "primary_value": null + }, + { + "domain": "number", + "entity_id": "number.gp9_param121", + "original_name": "param121", + "original_device_class": null, + "disabled": true, + "disabled_by": "integration", + "hidden_by": null, + "original_icon": null, + "entity_category": "config", + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-112-0-121", + "primary_value": null + }, + { + "domain": "switch", + "entity_id": "switch.gp9", + "original_name": "", + "original_device_class": null, + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": null, + "value_id": "46-37-2-currentValue", + "primary_value": { + "command_class": 37, + "command_class_name": "Binary Switch", + "endpoint": 2, + "property": "currentValue", + "property_name": "currentValue", + "property_key": null, + "property_key_name": null + } + }, + { + "domain": "sensor", + "entity_id": "sensor.gp9_electric_w_10", + "original_name": "Electric [W]", + "original_device_class": "power", + "disabled": false, + "disabled_by": null, + "hidden_by": null, + "original_icon": null, + "entity_category": null, + "supported_features": 0, + "unit_of_measurement": "W", + "value_id": "46-50-10-value-66051", + "primary_value": { + "command_class": 50, + "command_class_name": "Meter", + "endpoint": 10, + "property": "value", + "property_name": "value", + "property_key": 66051, + "property_key_name": "Electric_W_unknown (0x03)" + } + } + ], + "state": { + "nodeId": 46, + "index": 0, + "installerIcon": 2048, + "userIcon": 2048, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 351, + "productId": 33030, + "productType": 36865, + "firmwareVersion": "4.13.8", + "zwavePlusVersion": 2, + "name": "gp9", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/root/zwave/store/config/devices/0x015f/glass9.json", + "isEmbedded": false, + "manufacturer": "TouchWand Co., Ltd.", + "manufacturerId": 351, + "label": "Glass9", + "description": "Glass 9", + "devices": [ + { + "productType": 36865, + "productId": 33030 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "compat": { + "removeCCs": {} + } + }, + "label": "Glass9", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 13, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x015f:0x9001:0x8106:4.13.8", + "statistics": { + "commandsTX": 829, + "commandsRX": 923, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 4, + "rtt": 224.8, + "lastSeen": "2024-08-06T04:06:52.580Z", + "rssi": -49, + "lwr": { + "protocolDataRate": 3, + "repeaters": [], + "rssi": -49, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2024-08-06T04:06:15.046Z", + "protocol": 0, + "values": { + "46-91-0-slowRefresh": { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms.", + "label": "Send held down notifications at a slow rate", + "stateful": true, + "secret": false + } + }, + "46-114-0-manufacturerId": { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 351 + }, + "46-114-0-productType": { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 36865 + }, + "46-114-0-productId": { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 33030 + }, + "46-134-0-libraryType": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + "46-134-0-protocolVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.18" + }, + "46-134-0-firmwareVersions": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["4.13", "2.0"] + }, + "46-134-0-hardwareVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + "46-134-0-sdkVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.18.8" + }, + "46-134-0-applicationFrameworkAPIVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "10.18.8" + }, + "46-134-0-applicationFrameworkBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 437 + }, + "46-134-0-hostInterfaceVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + "46-134-0-hostInterfaceBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-134-0-zWaveProtocolVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.18.8" + }, + "46-134-0-zWaveProtocolBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 437 + }, + "46-134-0-applicationVersion": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "4.13.8" + }, + "46-134-0-applicationBuildNumber": { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 437 + }, + "46-49-1-Air temperature": { + "endpoint": 1, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 31.1, + "nodeId": 46 + }, + "46-37-2-currentValue": { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-2-targetValue": { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-2-duration": { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-2-sceneId": { + "endpoint": 2, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-2-dimmingDuration": { + "endpoint": 2, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-3-currentValue": { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-3-targetValue": { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-3-duration": { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-3-sceneId": { + "endpoint": 3, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-3-dimmingDuration": { + "endpoint": 3, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-4-currentValue": { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-4-targetValue": { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-4-duration": { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-4-sceneId": { + "endpoint": 4, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-4-dimmingDuration": { + "endpoint": 4, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-5-currentValue": { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-5-targetValue": { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-5-duration": { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-5-sceneId": { + "endpoint": 5, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-5-dimmingDuration": { + "endpoint": 5, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-6-currentValue": { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-6-targetValue": { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-6-duration": { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-6-sceneId": { + "endpoint": 6, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-6-dimmingDuration": { + "endpoint": 6, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-7-currentValue": { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-7-targetValue": { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-7-duration": { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-7-sceneId": { + "endpoint": 7, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-7-dimmingDuration": { + "endpoint": 7, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-8-currentValue": { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-8-targetValue": { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-8-duration": { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-38-8-targetValue": { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 63 + }, + "46-38-8-currentValue": { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-38-8-Up": { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + "46-38-8-Down": { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + "46-38-8-duration": { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + "46-38-8-restorePrevious": { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + "46-43-8-sceneId": { + "endpoint": 8, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-8-dimmingDuration": { + "endpoint": 8, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-50-8-value-65537": { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-50-8-value-66049": { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 8.5 + }, + "46-50-8-value-66561": { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [V]", + "ccSpecific": { + "meterType": 1, + "scale": 4, + "rateType": 1 + }, + "unit": "V", + "stateful": true, + "secret": false + }, + "value": 231.8 + }, + "46-50-8-value-66817": { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [A]", + "ccSpecific": { + "meterType": 1, + "scale": 5, + "rateType": 1 + }, + "unit": "A", + "stateful": true, + "secret": false + }, + "value": 0.04 + }, + "46-50-8-reset": { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + "46-113-8-alarmType": { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-113-8-alarmLevel": { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-113-8-Power Management-Over-current status": { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + } + }, + "46-37-9-currentValue": { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-9-targetValue": { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-9-duration": { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-38-9-targetValue": { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-38-9-currentValue": { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 54 + }, + "46-38-9-Up": { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + "46-38-9-Down": { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + "46-38-9-duration": { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + "46-38-9-restorePrevious": { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + "46-43-9-sceneId": { + "endpoint": 9, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-9-dimmingDuration": { + "endpoint": 9, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-50-9-value-65537": { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-50-9-value-66049": { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 8.7 + }, + "46-50-9-value-66561": { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [V]", + "ccSpecific": { + "meterType": 1, + "scale": 4, + "rateType": 1 + }, + "unit": "V", + "stateful": true, + "secret": false + }, + "value": 231.8 + }, + "46-50-9-value-66817": { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [A]", + "ccSpecific": { + "meterType": 1, + "scale": 5, + "rateType": 1 + }, + "unit": "A", + "stateful": true, + "secret": false + }, + "value": 0.04 + }, + "46-50-9-reset": { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + "46-113-9-alarmType": { + "endpoint": 9, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-113-9-alarmLevel": { + "endpoint": 9, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-113-9-Power Management-Over-current status": { + "endpoint": 9, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + } + }, + "46-37-10-currentValue": { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-10-targetValue": { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-10-duration": { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-38-10-targetValue": { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 56 + }, + "46-38-10-currentValue": { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-38-10-Up": { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + "46-38-10-Down": { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + "46-38-10-duration": { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + "46-38-10-restorePrevious": { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + "46-43-10-sceneId": { + "endpoint": 10, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-10-dimmingDuration": { + "endpoint": 10, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-50-10-value-65537": { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-50-10-value-66049": { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 9 + }, + "46-50-10-value-66561": { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [V]", + "ccSpecific": { + "meterType": 1, + "scale": 4, + "rateType": 1 + }, + "unit": "V", + "stateful": true, + "secret": false + }, + "value": 231.8 + }, + "46-50-10-value-66817": { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [A]", + "ccSpecific": { + "meterType": 1, + "scale": 5, + "rateType": 1 + }, + "unit": "A", + "stateful": true, + "secret": false + }, + "value": 0.04 + }, + "46-50-10-reset": { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + "46-113-10-alarmType": { + "endpoint": 10, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-113-10-alarmLevel": { + "endpoint": 10, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + "46-113-10-Power Management-Over-current status": { + "endpoint": 10, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + } + }, + "46-37-11-currentValue": { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-11-targetValue": { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-11-duration": { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-11-sceneId": { + "endpoint": 11, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-11-dimmingDuration": { + "endpoint": 11, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-12-currentValue": { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-12-targetValue": { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + "46-37-12-duration": { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-12-sceneId": { + "endpoint": 12, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-12-dimmingDuration": { + "endpoint": 12, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-37-13-currentValue": { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": true + }, + "46-37-13-targetValue": { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": true + }, + "46-37-13-duration": { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + "46-43-13-sceneId": { + "endpoint": 13, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + "46-43-13-dimmingDuration": { + "endpoint": 13, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + "46-50-10-value-66051": { + "commandClassName": "Meter", + "commandClass": 50, + "property": "value", + "propertyKey": 66051, + "endpoint": 10, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 3 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "propertyName": "value", + "propertyKeyName": "Electric_W_unknown (0x03)", + "nodeId": 46, + "value": 9.2 + } + }, + "endpoints": { + "0": { + "nodeId": 46, + "index": 0, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + } + ] + }, + "1": { + "nodeId": 46, + "index": 1, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "2": { + "nodeId": 46, + "index": 2, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "3": { + "nodeId": 46, + "index": 3, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "4": { + "nodeId": 46, + "index": 4, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "5": { + "nodeId": 46, + "index": 5, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "6": { + "nodeId": 46, + "index": 6, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "7": { + "nodeId": 46, + "index": 7, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "8": { + "nodeId": 46, + "index": 8, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "9": { + "nodeId": 46, + "index": 9, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "10": { + "nodeId": 46, + "index": 10, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "11": { + "nodeId": 46, + "index": 11, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "12": { + "nodeId": 46, + "index": 12, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + "13": { + "nodeId": 46, + "index": 13, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + } + } + } + } +} diff --git a/tests/components/zwave_js/fixtures/touchwand_glass9_state.json b/tests/components/zwave_js/fixtures/touchwand_glass9_state.json new file mode 100644 index 00000000000..a84797b75d4 --- /dev/null +++ b/tests/components/zwave_js/fixtures/touchwand_glass9_state.json @@ -0,0 +1,3467 @@ +{ + "nodeId": 46, + "index": 0, + "installerIcon": 2048, + "userIcon": 2048, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 351, + "productId": 33030, + "productType": 36865, + "firmwareVersion": "4.13.8", + "zwavePlusVersion": 2, + "name": "gp9", + "location": "**REDACTED**", + "deviceConfig": { + "filename": "/root/zwave/store/config/devices/0x015f/glass9.json", + "isEmbedded": false, + "manufacturer": "TouchWand Co., Ltd.", + "manufacturerId": 351, + "label": "Glass9", + "description": "Glass 9", + "devices": [ + { + "productType": 36865, + "productId": 33030 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "preferred": false, + "compat": { + "removeCCs": {} + } + }, + "label": "Glass9", + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 13, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x015f:0x9001:0x8106:4.13.8", + "statistics": { + "commandsTX": 829, + "commandsRX": 923, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 4, + "rtt": 224.8, + "lastSeen": "2024-08-06T04:06:52.580Z", + "rssi": -49, + "lwr": { + "protocolDataRate": 3, + "repeaters": [], + "rssi": -49, + "repeaterRSSI": [] + } + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "lastSeen": "2024-08-06T04:06:15.046Z", + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms.", + "label": "Send held down notifications at a slow rate", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 351 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 36865 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 33030 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.18" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + }, + "value": ["4.13", "2.0"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version", + "stateful": true, + "secret": false + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version", + "stateful": true, + "secret": false + }, + "value": "7.18.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version", + "stateful": true, + "secret": false + }, + "value": "10.18.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number", + "stateful": true, + "secret": false + }, + "value": 437 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version", + "stateful": true, + "secret": false + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + }, + "value": "7.18.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number", + "stateful": true, + "secret": false + }, + "value": 437 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version", + "stateful": true, + "secret": false + }, + "value": "4.13.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number", + "stateful": true, + "secret": false + }, + "value": 437 + }, + { + "endpoint": 1, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C", + "stateful": true, + "secret": false + }, + "value": 31.1, + "nodeId": 46 + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 2, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 2, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 2, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 3, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 3, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 3, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 4, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 4, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 5, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 5, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 5, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 6, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 6, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 6, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 7, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 7, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 7, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 8, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 63 + }, + { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 8.5 + }, + { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [V]", + "ccSpecific": { + "meterType": 1, + "scale": 4, + "rateType": 1 + }, + "unit": "V", + "stateful": true, + "secret": false + }, + "value": 231.8 + }, + { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [A]", + "ccSpecific": { + "meterType": 1, + "scale": 5, + "rateType": 1 + }, + "unit": "A", + "stateful": true, + "secret": false + }, + "value": 0.04 + }, + { + "endpoint": 8, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 8, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 9, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 54 + }, + { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 8.7 + }, + { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [V]", + "ccSpecific": { + "meterType": 1, + "scale": 4, + "rateType": 1 + }, + "unit": "V", + "stateful": true, + "secret": false + }, + "value": 231.8 + }, + { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [A]", + "ccSpecific": { + "meterType": 1, + "scale": 5, + "rateType": 1 + }, + "unit": "A", + "stateful": true, + "secret": false + }, + "value": 0.04 + }, + { + "endpoint": 9, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 9, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 9, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 9, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 10, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 56 + }, + { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + }, + "valueChangeOptions": ["transitionDuration"], + "states": { + "true": "Start", + "false": "Stop" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "restorePrevious", + "propertyName": "restorePrevious", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Restore previous value", + "states": { + "true": "Restore" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [kWh]", + "ccSpecific": { + "meterType": 1, + "scale": 0, + "rateType": 1 + }, + "unit": "kWh", + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 1 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 9 + }, + { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [V]", + "ccSpecific": { + "meterType": 1, + "scale": 4, + "rateType": 1 + }, + "unit": "V", + "stateful": true, + "secret": false + }, + "value": 231.8 + }, + { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "Electric_A_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumption [A]", + "ccSpecific": { + "meterType": 1, + "scale": 5, + "rateType": 1 + }, + "unit": "A", + "stateful": true, + "secret": false + }, + "value": 0.04 + }, + { + "endpoint": 10, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values", + "states": { + "true": "Reset" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 10, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 10, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255, + "stateful": true, + "secret": false + }, + "value": 0 + }, + { + "endpoint": 10, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-current status", + "propertyName": "Power Management", + "propertyKeyName": "Over-current status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Over-current status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "6": "Over-current detected" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 11, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 11, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 11, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 12, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 12, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 12, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value", + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value", + "valueChangeOptions": ["transitionDuration"], + "stateful": true, + "secret": false + }, + "value": true + }, + { + "endpoint": 13, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": false, + "label": "Remaining duration", + "stateful": true, + "secret": false + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 13, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "sceneId", + "propertyName": "sceneId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Scene ID", + "valueChangeOptions": ["transitionDuration"], + "min": 1, + "max": 255, + "stateful": false, + "secret": false + } + }, + { + "endpoint": 13, + "commandClass": 43, + "commandClassName": "Scene Activation", + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Dimming duration", + "stateful": true, + "secret": false + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "property": "value", + "propertyKey": 66051, + "endpoint": 10, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric [W]", + "ccSpecific": { + "meterType": 1, + "scale": 2, + "rateType": 3 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "propertyName": "value", + "propertyKeyName": "Electric_W_unknown (0x03)", + "nodeId": 46, + "value": 9.2 + } + ], + "endpoints": [ + { + "nodeId": 46, + "index": 0, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 1, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 2, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 3, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 4, + "installerIcon": 1792, + "userIcon": 1792, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 5, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 6, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 7, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 8, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 9, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 10, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 17, + "label": "Multilevel Switch" + }, + "specific": { + "key": 7, + "label": "Motor Control Class C" + } + }, + "commandClasses": [ + { + "id": 38, + "name": "Multilevel Switch", + "version": 4, + "isSecure": false + }, + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 11, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 12, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + }, + { + "nodeId": 46, + "index": 13, + "installerIcon": 2048, + "userIcon": 2048, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 0, + "label": "Unused" + } + }, + "commandClasses": [ + { + "id": 37, + "name": "Binary Switch", + "version": 2, + "isSecure": false + }, + { + "id": 43, + "name": "Scene Activation", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 02296262d1f..c8bfca2b35f 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -56,6 +56,24 @@ async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration) assert state +async def test_touchwand_glass9( + hass: HomeAssistant, + client: MagicMock, + touchwand_glass9: Node, + integration: MockConfigEntry, +) -> None: + """Test a touchwand_glass9 is discovered as a cover.""" + node = touchwand_glass9 + node_device_class = node.device_class + assert node_device_class + assert node_device_class.specific.label == "Unused" + + assert not hass.states.async_entity_ids_count("light") + assert hass.states.async_entity_ids_count("cover") == 3 + state = hass.states.get("cover.gp9") + assert state + + async def test_zvidar_state(hass: HomeAssistant, client, zvidar, integration) -> None: """Test that an ZVIDAR Z-CM-V01 multilevel switch value is discovered as a cover.""" node = zvidar diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index fa82b051e59..4350d7f7649 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -27,7 +27,7 @@ from homeassistant.components.persistent_notification import async_dismiss from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id, get_device_id_ext from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import CoreState, HomeAssistant from homeassistant.helpers import ( area_registry as ar, @@ -366,6 +366,7 @@ async def test_listen_done_after_setup( @pytest.mark.usefixtures("client") +@pytest.mark.parametrize("platforms", [[Platform.SENSOR]]) async def test_new_entity_on_value_added( hass: HomeAssistant, multisensor_6: Node, diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index c3580df1f27..ef77e22bbec 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -655,6 +655,17 @@ async def test_special_meters( "value": 659.813, }, ) + node_data["endpoints"].append( + { + "nodeId": 102, + "index": 10, + "installerIcon": 1792, + "userIcon": 1792, + "commandClasses": [ + {"id": 50, "name": "Meter", "version": 3, "isSecure": False} + ], + } + ) # Add an ElectricScale.KILOVOLT_AMPERE_REACTIVE value to the state so we can test that # it is handled differently (no device class) node_data["values"].append( @@ -678,6 +689,17 @@ async def test_special_meters( "value": 659.813, }, ) + node_data["endpoints"].append( + { + "nodeId": 102, + "index": 11, + "installerIcon": 1792, + "userIcon": 1792, + "commandClasses": [ + {"id": 50, "name": "Meter", "version": 3, "isSecure": False} + ], + } + ) node = Node(client, node_data) event = {"node": node} client.driver.controller.emit("node added", event) From d16ec81727948154aa040b49fb0582de5803b298 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:10:06 +0200 Subject: [PATCH 27/69] Migrate justnimbus to use runtime_data (#147170) --- homeassistant/components/justnimbus/__init__.py | 15 ++++++--------- .../components/justnimbus/coordinator.py | 8 ++++++-- homeassistant/components/justnimbus/sensor.py | 9 +++------ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/justnimbus/__init__.py b/homeassistant/components/justnimbus/__init__.py index 123807d887c..5f369027b00 100644 --- a/homeassistant/components/justnimbus/__init__.py +++ b/homeassistant/components/justnimbus/__init__.py @@ -2,15 +2,14 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import DOMAIN, PLATFORMS -from .coordinator import JustNimbusCoordinator +from .const import PLATFORMS +from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool: """Set up JustNimbus from a config entry.""" if "zip_code" in entry.data: coordinator = JustNimbusCoordinator(hass, entry) @@ -18,13 +17,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/justnimbus/coordinator.py b/homeassistant/components/justnimbus/coordinator.py index a6945c45417..b51058a8e54 100644 --- a/homeassistant/components/justnimbus/coordinator.py +++ b/homeassistant/components/justnimbus/coordinator.py @@ -16,13 +16,17 @@ from .const import CONF_ZIP_CODE, DOMAIN _LOGGER = logging.getLogger(__name__) +type JustNimbusConfigEntry = ConfigEntry[JustNimbusCoordinator] + class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]): """Data update coordinator.""" - config_entry: ConfigEntry + config_entry: JustNimbusConfigEntry - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: JustNimbusConfigEntry + ) -> None: """Initialize the coordinator.""" super().__init__( hass, diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 1e288e272cd..88f12cad113 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, EntityCategory, @@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from . import JustNimbusCoordinator -from .const import DOMAIN +from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator from .entity import JustNimbusEntity @@ -102,16 +100,15 @@ SENSOR_TYPES = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: JustNimbusConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the JustNimbus sensor.""" - coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( JustNimbusSensor( device_id=entry.data[CONF_CLIENT_ID], description=description, - coordinator=coordinator, + coordinator=entry.runtime_data, ) for description in SENSOR_TYPES ) From 0a5d13f10466e595100045b8194a5b314e9eb08b Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Fri, 20 Jun 2025 08:10:44 +0200 Subject: [PATCH 28/69] fix and improve cover tests for homee (#147164) --- tests/components/homee/test_cover.py | 74 ++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/tests/components/homee/test_cover.py b/tests/components/homee/test_cover.py index 4f85b2dd7cc..a3e26abc52a 100644 --- a/tests/components/homee/test_cover.py +++ b/tests/components/homee/test_cover.py @@ -66,6 +66,35 @@ async def test_open_close_stop_cover( assert call[0] == (mock_homee.nodes[0].id, 1, index) +async def test_open_close_reverse_cover( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test opening the cover.""" + mock_homee.nodes = [build_mock_node("cover_with_position_slats.json")] + mock_homee.nodes[0].attributes[0].is_reversed = True + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_cover"}, + blocking=True, + ) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_cover"}, + blocking=True, + ) + + calls = mock_homee.set_value.call_args_list + assert calls[0][0] == (mock_homee.nodes[0].id, 1, 1) # Open + assert calls[1][0] == (mock_homee.nodes[0].id, 1, 0) # Close + + async def test_set_cover_position( hass: HomeAssistant, mock_homee: MagicMock, @@ -76,30 +105,29 @@ async def test_set_cover_position( await setup_integration(hass, mock_config_entry) - # Slats have a range of -45 to 90. await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_slats", ATTR_POSITION: 100}, + {ATTR_ENTITY_ID: "cover.test_cover", ATTR_POSITION: 100}, blocking=True, ) await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_slats", ATTR_POSITION: 0}, + {ATTR_ENTITY_ID: "cover.test_cover", ATTR_POSITION: 0}, blocking=True, ) await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.test_slats", ATTR_POSITION: 50}, + {ATTR_ENTITY_ID: "cover.test_cover", ATTR_POSITION: 50}, blocking=True, ) calls = mock_homee.set_value.call_args_list positions = [0, 100, 50] for call in calls: - assert call[0] == (1, 2, positions.pop(0)) + assert call[0] == (3, 2, positions.pop(0)) async def test_close_open_slats( @@ -137,6 +165,42 @@ async def test_close_open_slats( assert call[0] == (mock_homee.nodes[0].id, 2, index) +async def test_close_open_reversed_slats( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test closing and opening slats.""" + mock_homee.nodes = [build_mock_node("cover_with_slats_position.json")] + mock_homee.nodes[0].attributes[1].is_reversed = True + + await setup_integration(hass, mock_config_entry) + + attributes = hass.states.get("cover.test_slats").attributes + assert attributes.get("supported_features") == ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: "cover.test_slats"}, + blocking=True, + ) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.test_slats"}, + blocking=True, + ) + + calls = mock_homee.set_value.call_args_list + assert calls[0][0] == (mock_homee.nodes[0].id, 2, 2) # Close + assert calls[1][0] == (mock_homee.nodes[0].id, 2, 1) # Open + + async def test_set_slat_position( hass: HomeAssistant, mock_homee: MagicMock, From 73bed96a0f2ac2c6bde3394cfcee21e27e2ad57c Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Fri, 20 Jun 2025 08:11:20 +0200 Subject: [PATCH 29/69] remove unwanted attribute in homee sensor tests (#147158) --- tests/components/homee/fixtures/sensors.json | 21 ---- .../homee/snapshots/test_sensor.ambr | 109 +++++++++--------- tests/components/homee/test_sensor.py | 4 +- 3 files changed, 58 insertions(+), 76 deletions(-) diff --git a/tests/components/homee/fixtures/sensors.json b/tests/components/homee/fixtures/sensors.json index 50daa59c99f..1c743195a20 100644 --- a/tests/components/homee/fixtures/sensors.json +++ b/tests/components/homee/fixtures/sensors.json @@ -81,27 +81,6 @@ "data": "", "name": "" }, - { - "id": 34, - "node_id": 1, - "instance": 2, - "minimum": 0, - "maximum": 100, - "current_value": 100.0, - "target_value": 100.0, - "last_value": 100.0, - "unit": "%", - "step_value": 1.0, - "editable": 0, - "type": 8, - "state": 1, - "last_changed": 1709982926, - "changed_by": 1, - "changed_by_id": 0, - "based_on": 1, - "data": "", - "name": "" - }, { "id": 4, "node_id": 1, diff --git a/tests/components/homee/snapshots/test_sensor.ambr b/tests/components/homee/snapshots/test_sensor.ambr index b5975af2d54..4e4eb98f28c 100644 --- a/tests/components/homee/snapshots/test_sensor.ambr +++ b/tests/components/homee/snapshots/test_sensor.ambr @@ -52,59 +52,6 @@ 'state': '100.0', }) # --- -# name: test_sensor_snapshot[sensor.test_multisensor_battery_2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_multisensor_battery_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Battery', - 'platform': 'homee', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'battery_instance', - 'unique_id': '00055511EECC-1-34', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensor_snapshot[sensor.test_multisensor_battery_2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Test MultiSensor Battery', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.test_multisensor_battery_2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '100.0', - }) -# --- # name: test_sensor_snapshot[sensor.test_multisensor_current_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -490,6 +437,62 @@ 'state': '2000.0', }) # --- +# name: test_sensor_snapshot[sensor.test_multisensor_external_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_multisensor_external_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'External temperature', + 'platform': 'homee', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'external_temperature', + 'unique_id': '00055511EECC-1-34', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_snapshot[sensor.test_multisensor_external_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Test MultiSensor External temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_multisensor_external_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.6', + }) +# --- # name: test_sensor_snapshot[sensor.test_multisensor_floor_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/homee/test_sensor.py b/tests/components/homee/test_sensor.py index 14a9320ffa1..1d4ad4b0f66 100644 --- a/tests/components/homee/test_sensor.py +++ b/tests/components/homee/test_sensor.py @@ -47,7 +47,7 @@ async def test_up_down_values( assert hass.states.get("sensor.test_multisensor_state").state == OPEN_CLOSE_MAP[0] - attribute = mock_homee.nodes[0].attributes[28] + attribute = mock_homee.nodes[0].attributes[27] for i in range(1, 5): await async_update_attribute_value(hass, attribute, i) assert ( @@ -77,7 +77,7 @@ async def test_window_position( == WINDOW_MAP[0] ) - attribute = mock_homee.nodes[0].attributes[33] + attribute = mock_homee.nodes[0].attributes[32] for i in range(1, 3): await async_update_attribute_value(hass, attribute, i) assert ( From 2e21493c198731b0486400b0cc711faca478ef47 Mon Sep 17 00:00:00 2001 From: Krisjanis Lejejs Date: Fri, 20 Jun 2025 11:18:03 +0300 Subject: [PATCH 30/69] Bump hass-nabucasa from 0.102.0 to 0.103.0 (#147186) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 0d1aca60c8f..b5c73e08f3e 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.102.0"], + "requirements": ["hass-nabucasa==0.103.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index df0c6ef7452..3eb77beed93 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -36,7 +36,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==3.49.0 -hass-nabucasa==0.102.0 +hass-nabucasa==0.103.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250531.3 diff --git a/pyproject.toml b/pyproject.toml index 0213dbf27ab..4295c23740f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "ha-ffmpeg==3.2.2", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.102.0", + "hass-nabucasa==0.103.0", # hassil is indirectly imported from onboarding via the import chain # onboarding->cloud->assist_pipeline->conversation->hassil. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its diff --git a/requirements.txt b/requirements.txt index bf963ecc52d..b47d33e7a44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 ha-ffmpeg==3.2.2 -hass-nabucasa==0.102.0 +hass-nabucasa==0.103.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index cf683a09e67..367bd2f5048 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1124,7 +1124,7 @@ habiticalib==0.4.0 habluetooth==3.49.0 # homeassistant.components.cloud -hass-nabucasa==0.102.0 +hass-nabucasa==0.103.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f513185014..abede3d5e7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -982,7 +982,7 @@ habiticalib==0.4.0 habluetooth==3.49.0 # homeassistant.components.cloud -hass-nabucasa==0.102.0 +hass-nabucasa==0.103.0 # homeassistant.components.assist_satellite # homeassistant.components.conversation From 973700542b062e11139c21650d01adbdb14c07ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:19:19 +0200 Subject: [PATCH 31/69] Move kmtronic coordinator to separate module (#147182) --- homeassistant/components/kmtronic/__init__.py | 30 ++---------- .../components/kmtronic/coordinator.py | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/kmtronic/coordinator.py diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index edec0b32af2..b49efebc35e 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -1,10 +1,5 @@ """The kmtronic integration.""" -import asyncio -from datetime import timedelta -import logging - -import aiohttp from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI @@ -12,14 +7,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER, UPDATE_LISTENER +from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, UPDATE_LISTENER +from .coordinator import KMtronicCoordinator PLATFORMS = [Platform.SWITCH] -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up kmtronic from a config entry.""" @@ -31,24 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], ) hub = KMTronicHubAPI(auth) - - async def async_update_data(): - try: - async with asyncio.timeout(10): - await hub.async_update_relays() - except aiohttp.client_exceptions.ClientResponseError as err: - raise UpdateFailed(f"Wrong credentials: {err}") from err - except aiohttp.client_exceptions.ClientConnectorError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - name=f"{MANUFACTURER} {hub.name}", - update_method=async_update_data, - update_interval=timedelta(seconds=30), - ) + coordinator = KMtronicCoordinator(hass, entry, hub) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/kmtronic/coordinator.py b/homeassistant/components/kmtronic/coordinator.py new file mode 100644 index 00000000000..8a94949dea6 --- /dev/null +++ b/homeassistant/components/kmtronic/coordinator.py @@ -0,0 +1,46 @@ +"""The kmtronic integration.""" + +import asyncio +from datetime import timedelta +import logging + +from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError +from pykmtronic.hub import KMTronicHubAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import MANUFACTURER + +PLATFORMS = [Platform.SWITCH] + +_LOGGER = logging.getLogger(__name__) + + +class KMtronicCoordinator(DataUpdateCoordinator[None]): + """Coordinator for KMTronic.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, hub: KMTronicHubAPI + ) -> None: + """Initialize the KMTronic coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=f"{MANUFACTURER} {hub.name}", + update_interval=timedelta(seconds=30), + ) + self.hub = hub + + async def _async_update_data(self) -> None: + """Fetch the latest data from the source.""" + try: + async with asyncio.timeout(10): + await self.hub.async_update_relays() + except ClientResponseError as err: + raise UpdateFailed(f"Wrong credentials: {err}") from err + except ClientConnectorError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err From e23cac8befb080a3e7e1a4a31f9c9a7d7bc7c052 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:23:41 +0200 Subject: [PATCH 32/69] Simplify remove listener in kodi (#147183) --- homeassistant/components/kodi/__init__.py | 12 ++---------- homeassistant/components/kodi/const.py | 1 - 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index d3c7d4da724..5400d142f28 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -17,13 +17,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( - CONF_WS_PORT, - DATA_CONNECTION, - DATA_KODI, - DATA_REMOVE_LISTENER, - DOMAIN, -) +from .const import CONF_WS_PORT, DATA_CONNECTION, DATA_KODI, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.MEDIA_PLAYER] @@ -58,13 +52,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _close(event): await conn.close() - remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_CONNECTION: conn, DATA_KODI: kodi, - DATA_REMOVE_LISTENER: remove_stop_listener, } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -78,6 +71,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: data = hass.data[DOMAIN].pop(entry.entry_id) await data[DATA_CONNECTION].close() - data[DATA_REMOVE_LISTENER]() return unload_ok diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py index 479b02e0fb5..167ea2a4725 100644 --- a/homeassistant/components/kodi/const.py +++ b/homeassistant/components/kodi/const.py @@ -6,7 +6,6 @@ CONF_WS_PORT = "ws_port" DATA_CONNECTION = "connection" DATA_KODI = "kodi" -DATA_REMOVE_LISTENER = "remove_listener" DEFAULT_PORT = 8080 DEFAULT_SSL = False From d0e77eb1e26a0e126b67dc933a0ce921f35650ee Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:24:56 +0200 Subject: [PATCH 33/69] Migrate keymitt_ble to use runtime_data (#147179) --- .../components/keymitt_ble/__init__.py | 19 +++++-------------- .../components/keymitt_ble/coordinator.py | 7 ++++--- .../components/keymitt_ble/entity.py | 2 +- .../components/keymitt_ble/switch.py | 9 +++------ 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/keymitt_ble/__init__.py b/homeassistant/components/keymitt_ble/__init__.py index 7fea46d7a02..01948006852 100644 --- a/homeassistant/components/keymitt_ble/__init__.py +++ b/homeassistant/components/keymitt_ble/__init__.py @@ -2,26 +2,20 @@ from __future__ import annotations -import logging - from microbot import MicroBotApiClient from homeassistant.components import bluetooth -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN -from .coordinator import MicroBotDataUpdateCoordinator +from .coordinator import MicroBotConfigEntry, MicroBotDataUpdateCoordinator -_LOGGER: logging.Logger = logging.getLogger(__package__) PLATFORMS: list[str] = [Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool: """Set up this integration using UI.""" - hass.data.setdefault(DOMAIN, {}) token: str = entry.data[CONF_ACCESS_TOKEN] bdaddr: str = entry.data[CONF_ADDRESS] ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr) @@ -35,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, client=client, ble_device=ble_device ) - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(coordinator.async_start()) @@ -43,9 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: MicroBotConfigEntry) -> bool: """Handle removal of an entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/keymitt_ble/coordinator.py b/homeassistant/components/keymitt_ble/coordinator.py index 3e72826ac5d..9d2b250ba82 100644 --- a/homeassistant/components/keymitt_ble/coordinator.py +++ b/homeassistant/components/keymitt_ble/coordinator.py @@ -11,14 +11,15 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothDataUpdateCoordinator, ) -from homeassistant.const import Platform +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback if TYPE_CHECKING: from bleak.backends.device import BLEDevice _LOGGER: logging.Logger = logging.getLogger(__package__) -PLATFORMS: list[str] = [Platform.SWITCH] + +type MicroBotConfigEntry = ConfigEntry[MicroBotDataUpdateCoordinator] class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): @@ -31,7 +32,7 @@ class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): ble_device: BLEDevice, ) -> None: """Initialize.""" - self.api: MicroBotApiClient = client + self.api = client self.data: dict[str, Any] = {} self.ble_device = ble_device super().__init__( diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py index b5229e6917e..94bb1498744 100644 --- a/homeassistant/components/keymitt_ble/entity.py +++ b/homeassistant/components/keymitt_ble/entity.py @@ -19,7 +19,7 @@ class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordin _attr_has_entity_name = True - def __init__(self, coordinator, config_entry): + def __init__(self, coordinator: MicroBotDataUpdateCoordinator) -> None: """Initialise the entity.""" super().__init__(coordinator) self._address = self.coordinator.ble_device.address diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 57d3af98062..dab7d8c2d36 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -7,7 +7,6 @@ from typing import Any import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import ( @@ -16,8 +15,7 @@ from homeassistant.helpers.entity_platform import ( ) from homeassistant.helpers.typing import VolDictType -from .const import DOMAIN -from .coordinator import MicroBotDataUpdateCoordinator +from .coordinator import MicroBotConfigEntry from .entity import MicroBotEntity CALIBRATE = "calibrate" @@ -30,12 +28,11 @@ CALIBRATE_SCHEMA: VolDictType = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: MicroBotConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up MicroBot based on a config entry.""" - coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([MicroBotBinarySwitch(coordinator, entry)]) + async_add_entities([MicroBotBinarySwitch(entry.runtime_data)]) platform = async_get_current_platform() platform.async_register_entity_service( CALIBRATE, From e315cb9859e4af7b0b098aed7cbe8fc5617474ea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:25:08 +0200 Subject: [PATCH 34/69] Migrate kostal_plenticore to use runtime_data (#147188) --- .../components/kostal_plenticore/__init__.py | 19 ++++++------------- .../kostal_plenticore/coordinator.py | 10 ++++++---- .../kostal_plenticore/diagnostics.py | 8 +++----- .../components/kostal_plenticore/number.py | 8 +++----- .../components/kostal_plenticore/select.py | 8 +++----- .../components/kostal_plenticore/sensor.py | 8 +++----- .../components/kostal_plenticore/switch.py | 8 +++----- .../kostal_plenticore/test_helper.py | 4 ++-- 8 files changed, 29 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index 3675b4342b4..c549a8d338f 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -4,42 +4,35 @@ import logging from pykoplenti import ApiException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import Plenticore +from .coordinator import Plenticore, PlenticoreConfigEntry _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: PlenticoreConfigEntry) -> bool: """Set up Kostal Plenticore Solar Inverter from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - plenticore = Plenticore(hass, entry) if not await plenticore.async_setup(): return False - hass.data[DOMAIN][entry.entry_id] = plenticore + entry.runtime_data = plenticore await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: PlenticoreConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - # remove API object - plenticore = hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): try: - await plenticore.async_unload() + await entry.runtime_data.async_unload() except ApiException as err: _LOGGER.error("Error logging out from inverter: %s", err) diff --git a/homeassistant/components/kostal_plenticore/coordinator.py b/homeassistant/components/kostal_plenticore/coordinator.py index f87f8ca630a..d312130bb54 100644 --- a/homeassistant/components/kostal_plenticore/coordinator.py +++ b/homeassistant/components/kostal_plenticore/coordinator.py @@ -30,6 +30,8 @@ from .helper import get_hostname_id _LOGGER = logging.getLogger(__name__) +type PlenticoreConfigEntry = ConfigEntry[Plenticore] + class Plenticore: """Manages the Plenticore API.""" @@ -166,12 +168,12 @@ class DataUpdateCoordinatorMixin: class PlenticoreUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Base implementation of DataUpdateCoordinator for Plenticore data.""" - config_entry: ConfigEntry + config_entry: PlenticoreConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: PlenticoreConfigEntry, logger: logging.Logger, name: str, update_inverval: timedelta, @@ -248,12 +250,12 @@ class SettingDataUpdateCoordinator( class PlenticoreSelectUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Base implementation of DataUpdateCoordinator for Plenticore data.""" - config_entry: ConfigEntry + config_entry: PlenticoreConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: PlenticoreConfigEntry, logger: logging.Logger, name: str, update_inverval: timedelta, diff --git a/homeassistant/components/kostal_plenticore/diagnostics.py b/homeassistant/components/kostal_plenticore/diagnostics.py index 3978869c524..4d4d61f56a7 100644 --- a/homeassistant/components/kostal_plenticore/diagnostics.py +++ b/homeassistant/components/kostal_plenticore/diagnostics.py @@ -5,23 +5,21 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import REDACTED, async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_IDENTIFIERS, CONF_PASSWORD from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import Plenticore +from .coordinator import PlenticoreConfigEntry TO_REDACT = {CONF_PASSWORD} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: PlenticoreConfigEntry ) -> dict[str, dict[str, Any]]: """Return diagnostics for a config entry.""" data = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)} - plenticore: Plenticore = hass.data[DOMAIN][config_entry.entry_id] + plenticore = config_entry.runtime_data # Get information from Kostal Plenticore library available_process_data = await plenticore.client.get_process_data() diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 7efb00cf8f4..ddb0a84a6cc 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -14,15 +14,13 @@ from homeassistant.components.number import ( NumberEntityDescription, NumberMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN -from .coordinator import SettingDataUpdateCoordinator +from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator from .helper import PlenticoreDataFormatter _LOGGER = logging.getLogger(__name__) @@ -74,11 +72,11 @@ NUMBER_SETTINGS_DATA = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: PlenticoreConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add Kostal Plenticore Number entities.""" - plenticore = hass.data[DOMAIN][entry.entry_id] + plenticore = entry.runtime_data entities = [] diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 61929b9fadc..86ffb63966d 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -7,15 +7,13 @@ from datetime import timedelta import logging from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN -from .coordinator import Plenticore, SelectDataUpdateCoordinator +from .coordinator import PlenticoreConfigEntry, SelectDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -43,11 +41,11 @@ SELECT_SETTINGS_DATA = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: PlenticoreConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add kostal plenticore Select widget.""" - plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id] + plenticore = entry.runtime_data available_settings_data = await plenticore.client.get_settings() select_data_update_coordinator = SelectDataUpdateCoordinator( diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 1be7fb06e7b..aafd6bb1ff6 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -29,8 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN -from .coordinator import ProcessDataUpdateCoordinator +from .coordinator import PlenticoreConfigEntry, ProcessDataUpdateCoordinator from .helper import PlenticoreDataFormatter _LOGGER = logging.getLogger(__name__) @@ -808,11 +806,11 @@ SENSOR_PROCESS_DATA = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: PlenticoreConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add kostal plenticore Sensors.""" - plenticore = hass.data[DOMAIN][entry.entry_id] + plenticore = entry.runtime_data entities = [] diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index e3d5f830c78..44eced7ca4a 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -8,15 +8,13 @@ import logging from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN -from .coordinator import SettingDataUpdateCoordinator +from .coordinator import PlenticoreConfigEntry, SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -49,11 +47,11 @@ SWITCH_SETTINGS_DATA = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: PlenticoreConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add kostal plenticore Switch.""" - plenticore = hass.data[DOMAIN][entry.entry_id] + plenticore = entry.runtime_data entities = [] diff --git a/tests/components/kostal_plenticore/test_helper.py b/tests/components/kostal_plenticore/test_helper.py index acd33f82a27..96cdc99144b 100644 --- a/tests/components/kostal_plenticore/test_helper.py +++ b/tests/components/kostal_plenticore/test_helper.py @@ -67,7 +67,7 @@ async def test_plenticore_async_setup_g1( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - plenticore = hass.data[DOMAIN][mock_config_entry.entry_id] + plenticore = mock_config_entry.runtime_data assert plenticore.device_info == DeviceInfo( configuration_url="http://192.168.1.2", @@ -119,7 +119,7 @@ async def test_plenticore_async_setup_g2( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - plenticore = hass.data[DOMAIN][mock_config_entry.entry_id] + plenticore = mock_config_entry.runtime_data assert plenticore.device_info == DeviceInfo( configuration_url="http://192.168.1.2", From 8f661fc5cf3f5cd8d04493487e11648f6425d885 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:26:53 +0200 Subject: [PATCH 35/69] Migrate kegtron to use runtime_data (#147177) --- homeassistant/components/kegtron/__init__.py | 28 +++++++++----------- homeassistant/components/kegtron/sensor.py | 10 +++---- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/kegtron/__init__.py b/homeassistant/components/kegtron/__init__.py index d7485be0840..ec2ebee6995 100644 --- a/homeassistant/components/kegtron/__init__.py +++ b/homeassistant/components/kegtron/__init__.py @@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN - PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +type KegtronConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool: """Set up Kegtron BLE device from a config entry.""" address = entry.unique_id assert address is not None data = KegtronBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( - PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, - ) + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( coordinator.async_start() @@ -42,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: KegtronConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index 602c61f96ff..f0023e8ef6a 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -8,11 +8,9 @@ from kegtron_ble import ( Units, ) -from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -30,7 +28,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info -from .const import DOMAIN +from . import KegtronConfigEntry from .device import device_key_to_bluetooth_entity_key SENSOR_DESCRIPTIONS = { @@ -109,13 +107,11 @@ def sensor_update_to_bluetooth_data_update( async def async_setup_entry( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: KegtronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Kegtron BLE sensors.""" - coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ - entry.entry_id - ] + coordinator = entry.runtime_data processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( From 32314dbb13f1bc5251835c1ab621213c579a70c6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:27:07 +0200 Subject: [PATCH 36/69] Simplify update_listener in kmtronic (#147184) --- homeassistant/components/kmtronic/__init__.py | 7 ++----- homeassistant/components/kmtronic/const.py | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index b49efebc35e..1c2cfb7cc31 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -8,7 +8,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platfor from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN, UPDATE_LISTENER +from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN from .coordinator import KMtronicCoordinator PLATFORMS = [Platform.SWITCH] @@ -35,8 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - update_listener = entry.add_update_listener(async_update_options) - hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener + entry.async_on_unload(entry.add_update_listener(async_update_options)) return True @@ -50,8 +49,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] - update_listener() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/kmtronic/const.py b/homeassistant/components/kmtronic/const.py index 3bdb3074851..2381ad57998 100644 --- a/homeassistant/components/kmtronic/const.py +++ b/homeassistant/components/kmtronic/const.py @@ -8,5 +8,3 @@ DATA_HUB = "hub" DATA_COORDINATOR = "coordinator" MANUFACTURER = "KMtronic" - -UPDATE_LISTENER = "update_listener" From 05343392a757967056792ab3ab6b7c5c606d5622 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:27:47 +0200 Subject: [PATCH 37/69] Simplify update_listener in keenetic_ndms2 (#147173) --- homeassistant/components/keenetic_ndms2/__init__.py | 6 +----- homeassistant/components/keenetic_ndms2/const.py | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index e2ca17ebce8..a4447dcd904 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -20,7 +20,6 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTER, - UNDO_UPDATE_LISTENER, ) from .router import KeeneticRouter @@ -36,11 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: router = KeeneticRouter(hass, entry) await router.async_setup() - undo_listener = entry.add_update_listener(update_listener) + entry.async_on_unload(entry.add_update_listener(update_listener)) hass.data[DOMAIN][entry.entry_id] = { ROUTER: router, - UNDO_UPDATE_LISTENER: undo_listener, } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -50,8 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() - unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS ) diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py index 0b415a9502f..d7db0673690 100644 --- a/homeassistant/components/keenetic_ndms2/const.py +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -6,7 +6,6 @@ from homeassistant.components.device_tracker import ( DOMAIN = "keenetic_ndms2" ROUTER = "router" -UNDO_UPDATE_LISTENER = "undo_update_listener" DEFAULT_TELNET_PORT = 23 DEFAULT_SCAN_INTERVAL = 120 DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds() From 8c1e43c07c5bb207a36e48b4dbbe00146edb0f98 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 20 Jun 2025 10:28:35 +0200 Subject: [PATCH 38/69] Bump pypck to 0.8.9 (#147174) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 30584bc33f6..9e300716d3e 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.8.8", "lcn-frontend==0.2.5"] + "requirements": ["pypck==0.8.9", "lcn-frontend==0.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 367bd2f5048..425d09bd2eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2237,7 +2237,7 @@ pypaperless==4.1.0 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.8.8 +pypck==0.8.9 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abede3d5e7d..924ebc07ef7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1858,7 +1858,7 @@ pypalazzetti==0.1.19 pypaperless==4.1.0 # homeassistant.components.lcn -pypck==0.8.8 +pypck==0.8.9 # homeassistant.components.pglab pypglab==0.0.5 From fde36d5034904c461df4992f01c2ecd7d1e9098a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:31:28 +0200 Subject: [PATCH 39/69] Simplify update_listener in konnected (#147172) --- homeassistant/components/konnected/__init__.py | 9 +-------- homeassistant/components/konnected/const.py | 2 -- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 25c731ac7f4..dd4dbc7dbe5 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -58,7 +58,6 @@ from .const import ( PIN_TO_ZONE, STATE_HIGH, STATE_LOW, - UNDO_UPDATE_LISTENER, UPDATE_ENDPOINT, ZONE_TO_PIN, ZONES, @@ -261,10 +260,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - # config entry specific data to enable unload - hass.data[DOMAIN][entry.entry_id] = { - UNDO_UPDATE_LISTENER: entry.add_update_listener(async_entry_updated) - } + entry.async_on_unload(entry.add_update_listener(async_entry_updated)) return True @@ -272,11 +268,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - if unload_ok: hass.data[DOMAIN][CONF_DEVICES].pop(entry.data[CONF_ID]) - hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py index c4dd67e7d39..ffaa548003b 100644 --- a/homeassistant/components/konnected/const.py +++ b/homeassistant/components/konnected/const.py @@ -44,5 +44,3 @@ ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} ENDPOINT_ROOT = "/api/konnected" UPDATE_ENDPOINT = ENDPOINT_ROOT + r"/device/{device_id:[a-zA-Z0-9]+}" SIGNAL_DS18B20_NEW = "konnected.ds18b20.new" - -UNDO_UPDATE_LISTENER = "undo_update_listener" From 84e94222544cc87f220fcc9a26471c629c584dfc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:33:17 +0200 Subject: [PATCH 40/69] Move juicenet coordinator to separate module (#147168) --- homeassistant/components/juicenet/__init__.py | 18 ++-------- .../components/juicenet/coordinator.py | 33 +++++++++++++++++++ homeassistant/components/juicenet/device.py | 10 +++--- homeassistant/components/juicenet/entity.py | 10 +++--- homeassistant/components/juicenet/number.py | 15 +++++---- homeassistant/components/juicenet/sensor.py | 17 +++++++--- homeassistant/components/juicenet/switch.py | 14 +++++--- 7 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/juicenet/coordinator.py diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index fcfca7f2492..6cfdd85c6b7 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,6 +1,5 @@ """The JuiceNet integration.""" -from datetime import timedelta import logging import aiohttp @@ -14,9 +13,9 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .coordinator import JuiceNetCoordinator from .device import JuiceNetApi _LOGGER = logging.getLogger(__name__) @@ -74,20 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False _LOGGER.debug("%d JuiceNet device(s) found", len(juicenet.devices)) - async def async_update_data(): - """Update all device states from the JuiceNet API.""" - for device in juicenet.devices: - await device.update_state(True) - return True - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - name="JuiceNet", - update_method=async_update_data, - update_interval=timedelta(seconds=30), - ) + coordinator = JuiceNetCoordinator(hass, entry, juicenet) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/juicenet/coordinator.py b/homeassistant/components/juicenet/coordinator.py new file mode 100644 index 00000000000..7a89416e400 --- /dev/null +++ b/homeassistant/components/juicenet/coordinator.py @@ -0,0 +1,33 @@ +"""The JuiceNet integration.""" + +from datetime import timedelta +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .device import JuiceNetApi + +_LOGGER = logging.getLogger(__name__) + + +class JuiceNetCoordinator(DataUpdateCoordinator[None]): + """Coordinator for JuiceNet.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, juicenet_api: JuiceNetApi + ) -> None: + """Initialize the JuiceNet coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name="JuiceNet", + update_interval=timedelta(seconds=30), + ) + self.juicenet_api = juicenet_api + + async def _async_update_data(self) -> None: + for device in self.juicenet_api.devices: + await device.update_state(True) diff --git a/homeassistant/components/juicenet/device.py b/homeassistant/components/juicenet/device.py index daec88c2a94..b38b0efd68a 100644 --- a/homeassistant/components/juicenet/device.py +++ b/homeassistant/components/juicenet/device.py @@ -1,19 +1,21 @@ """Adapter to wrap the pyjuicenet api for home assistant.""" +from pyjuicenet import Api, Charger + class JuiceNetApi: """Represent a connection to JuiceNet.""" - def __init__(self, api): + def __init__(self, api: Api) -> None: """Create an object from the provided API instance.""" self.api = api - self._devices = [] + self._devices: list[Charger] = [] - async def setup(self): + async def setup(self) -> None: """JuiceNet device setup.""" self._devices = await self.api.get_devices() @property - def devices(self) -> list: + def devices(self) -> list[Charger]: """Get a list of devices managed by this account.""" return self._devices diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index b3433948582..d54ccb5accb 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -3,21 +3,19 @@ from pyjuicenet import Charger from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import JuiceNetCoordinator -class JuiceNetDevice(CoordinatorEntity): +class JuiceNetEntity(CoordinatorEntity[JuiceNetCoordinator]): """Represent a base JuiceNet device.""" _attr_has_entity_name = True def __init__( - self, device: Charger, key: str, coordinator: DataUpdateCoordinator + self, device: Charger, key: str, coordinator: JuiceNetCoordinator ) -> None: """Initialise the sensor.""" super().__init__(coordinator) diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py index 69323884f61..ff8c357a115 100644 --- a/homeassistant/components/juicenet/number.py +++ b/homeassistant/components/juicenet/number.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass -from pyjuicenet import Api, Charger +from pyjuicenet import Charger from homeassistant.components.number import ( DEFAULT_MAX_VALUE, @@ -14,10 +14,11 @@ from homeassistant.components.number import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR -from .entity import JuiceNetDevice +from .coordinator import JuiceNetCoordinator +from .device import JuiceNetApi +from .entity import JuiceNetEntity @dataclass(frozen=True, kw_only=True) @@ -47,8 +48,8 @@ async def async_setup_entry( ) -> None: """Set up the JuiceNet Numbers.""" juicenet_data = hass.data[DOMAIN][config_entry.entry_id] - api: Api = juicenet_data[JUICENET_API] - coordinator = juicenet_data[JUICENET_COORDINATOR] + api: JuiceNetApi = juicenet_data[JUICENET_API] + coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR] entities = [ JuiceNetNumber(device, description, coordinator) @@ -58,7 +59,7 @@ async def async_setup_entry( async_add_entities(entities) -class JuiceNetNumber(JuiceNetDevice, NumberEntity): +class JuiceNetNumber(JuiceNetEntity, NumberEntity): """Implementation of a JuiceNet number.""" entity_description: JuiceNetNumberEntityDescription @@ -67,7 +68,7 @@ class JuiceNetNumber(JuiceNetDevice, NumberEntity): self, device: Charger, description: JuiceNetNumberEntityDescription, - coordinator: DataUpdateCoordinator, + coordinator: JuiceNetCoordinator, ) -> None: """Initialise the number.""" super().__init__(device, description.key, coordinator) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 7bf0639f5d0..e3ae35da2ce 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pyjuicenet import Charger + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -21,7 +23,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR -from .entity import JuiceNetDevice +from .coordinator import JuiceNetCoordinator +from .device import JuiceNetApi +from .entity import JuiceNetEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -74,8 +78,8 @@ async def async_setup_entry( ) -> None: """Set up the JuiceNet Sensors.""" juicenet_data = hass.data[DOMAIN][config_entry.entry_id] - api = juicenet_data[JUICENET_API] - coordinator = juicenet_data[JUICENET_COORDINATOR] + api: JuiceNetApi = juicenet_data[JUICENET_API] + coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR] entities = [ JuiceNetSensorDevice(device, coordinator, description) @@ -85,11 +89,14 @@ async def async_setup_entry( async_add_entities(entities) -class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): +class JuiceNetSensorDevice(JuiceNetEntity, SensorEntity): """Implementation of a JuiceNet sensor.""" def __init__( - self, device, coordinator, description: SensorEntityDescription + self, + device: Charger, + coordinator: JuiceNetCoordinator, + description: SensorEntityDescription, ) -> None: """Initialise the sensor.""" super().__init__(device, description.key, coordinator) diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py index 9f34b7afdb3..e8a16e9da8f 100644 --- a/homeassistant/components/juicenet/switch.py +++ b/homeassistant/components/juicenet/switch.py @@ -2,13 +2,17 @@ from typing import Any +from pyjuicenet import Charger + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR -from .entity import JuiceNetDevice +from .coordinator import JuiceNetCoordinator +from .device import JuiceNetApi +from .entity import JuiceNetEntity async def async_setup_entry( @@ -18,20 +22,20 @@ async def async_setup_entry( ) -> None: """Set up the JuiceNet switches.""" juicenet_data = hass.data[DOMAIN][config_entry.entry_id] - api = juicenet_data[JUICENET_API] - coordinator = juicenet_data[JUICENET_COORDINATOR] + api: JuiceNetApi = juicenet_data[JUICENET_API] + coordinator: JuiceNetCoordinator = juicenet_data[JUICENET_COORDINATOR] async_add_entities( JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices ) -class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): +class JuiceNetChargeNowSwitch(JuiceNetEntity, SwitchEntity): """Implementation of a JuiceNet switch.""" _attr_translation_key = "charge_now" - def __init__(self, device, coordinator): + def __init__(self, device: Charger, coordinator: JuiceNetCoordinator) -> None: """Initialise the switch.""" super().__init__(device, "charge_now", coordinator) From 88683a318d5137eb4b1b8ab9df90fb934b507b0a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 20 Jun 2025 10:34:43 +0200 Subject: [PATCH 41/69] Add support of taking a camera snapshot via go2rtc (#145205) --- homeassistant/components/camera/__init__.py | 4 + homeassistant/components/camera/webrtc.py | 9 + homeassistant/components/go2rtc/__init__.py | 105 +++++--- tests/common.py | 6 + tests/components/camera/test_init.py | 148 +++++++++--- tests/components/feedreader/__init__.py | 8 +- tests/components/feedreader/conftest.py | 27 +-- tests/components/go2rtc/__init__.py | 31 +++ tests/components/go2rtc/conftest.py | 121 +++++++++- tests/components/go2rtc/fixtures/snapshot.jpg | Bin 0 -> 293320 bytes tests/components/go2rtc/test_init.py | 225 ++++++------------ 11 files changed, 441 insertions(+), 243 deletions(-) create mode 100644 tests/components/go2rtc/fixtures/snapshot.jpg diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ee9d1cbc94f..8348c53cd1c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -240,6 +240,10 @@ async def _async_get_stream_image( height: int | None = None, wait_for_next_keyframe: bool = False, ) -> bytes | None: + if (provider := camera._webrtc_provider) and ( # noqa: SLF001 + image := await provider.async_get_image(camera, width=width, height=height) + ) is not None: + return image if not camera.stream and CameraEntityFeature.STREAM in camera.supported_features: camera.stream = await camera.async_create_stream() if camera.stream: diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index 9ad50430f83..c2de5eac0a0 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -156,6 +156,15 @@ class CameraWebRTCProvider(ABC): """Close the session.""" return ## This is an optional method so we need a default here. + async def async_get_image( + self, + camera: Camera, + width: int | None = None, + height: int | None = None, + ) -> bytes | None: + """Get an image from the camera.""" + return None + @callback def async_register_webrtc_provider( diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 31acdd2de50..4e15b93330c 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -1,8 +1,11 @@ """The go2rtc component.""" +from __future__ import annotations + import logging import shutil +from aiohttp import ClientSession from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError from awesomeversion import AwesomeVersion from go2rtc_client import Go2RtcRestClient @@ -32,7 +35,7 @@ from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOM from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import ( config_validation as cv, discovery_flow, @@ -98,6 +101,7 @@ CONFIG_SCHEMA = vol.Schema( _DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN) _RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError) +type Go2RtcConfigEntry = ConfigEntry[WebRTCProvider] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -151,13 +155,14 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None: await hass.config_entries.async_remove(entry.entry_id) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool: """Set up go2rtc from a config entry.""" - url = hass.data[_DATA_GO2RTC] + url = hass.data[_DATA_GO2RTC] + session = async_get_clientsession(hass) + client = Go2RtcRestClient(session, url) # Validate the server URL try: - client = Go2RtcRestClient(async_get_clientsession(hass), url) version = await client.validate_server_version() if version < AwesomeVersion(RECOMMENDED_VERSION): ir.async_create_issue( @@ -188,13 +193,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err) return False - provider = WebRTCProvider(hass, url) - async_register_webrtc_provider(hass, provider) + provider = entry.runtime_data = WebRTCProvider(hass, url, session, client) + entry.async_on_unload(async_register_webrtc_provider(hass, provider)) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool: """Unload a go2rtc config entry.""" + await entry.runtime_data.teardown() return True @@ -206,12 +212,18 @@ async def _get_binary(hass: HomeAssistant) -> str | None: class WebRTCProvider(CameraWebRTCProvider): """WebRTC provider.""" - def __init__(self, hass: HomeAssistant, url: str) -> None: + def __init__( + self, + hass: HomeAssistant, + url: str, + session: ClientSession, + rest_client: Go2RtcRestClient, + ) -> None: """Initialize the WebRTC provider.""" self._hass = hass self._url = url - self._session = async_get_clientsession(hass) - self._rest_client = Go2RtcRestClient(self._session, url) + self._session = session + self._rest_client = rest_client self._sessions: dict[str, Go2RtcWsClient] = {} @property @@ -232,32 +244,16 @@ class WebRTCProvider(CameraWebRTCProvider): send_message: WebRTCSendMessage, ) -> None: """Handle the WebRTC offer and return the answer via the provided callback.""" + try: + await self._update_stream_source(camera) + except HomeAssistantError as err: + send_message(WebRTCError("go2rtc_webrtc_offer_failed", str(err))) + return + self._sessions[session_id] = ws_client = Go2RtcWsClient( self._session, self._url, source=camera.entity_id ) - if not (stream_source := await camera.stream_source()): - send_message( - WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") - ) - return - - streams = await self._rest_client.streams.list() - - if (stream := streams.get(camera.entity_id)) is None or not any( - stream_source == producer.url for producer in stream.producers - ): - await self._rest_client.streams.add( - camera.entity_id, - [ - stream_source, - # We are setting any ffmpeg rtsp related logs to debug - # Connection problems to the camera will be logged by the first stream - # Therefore setting it to debug will not hide any important logs - f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", - ], - ) - @callback def on_messages(message: ReceiveMessages) -> None: """Handle messages.""" @@ -291,3 +287,48 @@ class WebRTCProvider(CameraWebRTCProvider): """Close the session.""" ws_client = self._sessions.pop(session_id) self._hass.async_create_task(ws_client.close()) + + async def async_get_image( + self, + camera: Camera, + width: int | None = None, + height: int | None = None, + ) -> bytes | None: + """Get an image from the camera.""" + await self._update_stream_source(camera) + return await self._rest_client.get_jpeg_snapshot( + camera.entity_id, width, height + ) + + async def _update_stream_source(self, camera: Camera) -> None: + """Update the stream source in go2rtc config if needed.""" + if not (stream_source := await camera.stream_source()): + await self.teardown() + raise HomeAssistantError("Camera has no stream source") + + if not self.async_is_supported(stream_source): + await self.teardown() + raise HomeAssistantError("Stream source is not supported by go2rtc") + + streams = await self._rest_client.streams.list() + + if (stream := streams.get(camera.entity_id)) is None or not any( + stream_source == producer.url for producer in stream.producers + ): + await self._rest_client.streams.add( + camera.entity_id, + [ + stream_source, + # We are setting any ffmpeg rtsp related logs to debug + # Connection problems to the camera will be logged by the first stream + # Therefore setting it to debug will not hide any important logs + f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + f"ffmpeg:{camera.entity_id}#video=mjpeg", + ], + ) + + async def teardown(self) -> None: + """Tear down the provider.""" + for ws_client in self._sessions.values(): + await ws_client.close() + self._sessions.clear() diff --git a/tests/common.py b/tests/common.py index 322a47c8a09..d184d2b46fb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -567,6 +567,12 @@ def get_fixture_path(filename: str, integration: str | None = None) -> pathlib.P ) +@lru_cache +def load_fixture_bytes(filename: str, integration: str | None = None) -> bytes: + """Load a fixture.""" + return get_fixture_path(filename, integration).read_bytes() + + @lru_cache def load_fixture(filename: str, integration: str | None = None) -> str: """Load a fixture.""" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 7c56d142920..839394edbef 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,5 +1,6 @@ """The tests for the camera component.""" +from collections.abc import Callable from http import HTTPStatus import io from types import ModuleType @@ -876,6 +877,41 @@ async def test_entity_picture_url_changes_on_token_update(hass: HomeAssistant) - assert "token=" in new_entity_picture +async def _register_test_webrtc_provider(hass: HomeAssistant) -> Callable[[], None]: + class SomeTestProvider(CameraWebRTCProvider): + """Test provider.""" + + @property + def domain(self) -> str: + """Return domain.""" + return "test" + + @callback + def async_is_supported(self, stream_source: str) -> bool: + """Determine if the provider supports the stream source.""" + return True + + async def async_handle_async_webrtc_offer( + self, + camera: Camera, + offer_sdp: str, + session_id: str, + send_message: WebRTCSendMessage, + ) -> None: + """Handle the WebRTC offer and return the answer via the provided callback.""" + send_message(WebRTCAnswer("answer")) + + async def async_on_webrtc_candidate( + self, session_id: str, candidate: RTCIceCandidateInit + ) -> None: + """Handle the WebRTC candidate.""" + + provider = SomeTestProvider() + unsub = async_register_webrtc_provider(hass, provider) + await hass.async_block_till_done() + return unsub + + async def _test_capabilities( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -908,38 +944,7 @@ async def _test_capabilities( await test(expected_stream_types) # Test with WebRTC provider - - class SomeTestProvider(CameraWebRTCProvider): - """Test provider.""" - - @property - def domain(self) -> str: - """Return domain.""" - return "test" - - @callback - def async_is_supported(self, stream_source: str) -> bool: - """Determine if the provider supports the stream source.""" - return True - - async def async_handle_async_webrtc_offer( - self, - camera: Camera, - offer_sdp: str, - session_id: str, - send_message: WebRTCSendMessage, - ) -> None: - """Handle the WebRTC offer and return the answer via the provided callback.""" - send_message(WebRTCAnswer("answer")) - - async def async_on_webrtc_candidate( - self, session_id: str, candidate: RTCIceCandidateInit - ) -> None: - """Handle the WebRTC candidate.""" - - provider = SomeTestProvider() - async_register_webrtc_provider(hass, provider) - await hass.async_block_till_done() + await _register_test_webrtc_provider(hass) await test(expected_stream_types_with_webrtc_provider) @@ -1026,3 +1031,82 @@ async def test_camera_capabilities_changing_native_support( await hass.async_block_till_done() await _test_capabilities(hass, hass_ws_client, cam.entity_id, set(), set()) + + +@pytest.mark.usefixtures("mock_camera", "mock_stream_source") +async def test_snapshot_service_webrtc_provider( + hass: HomeAssistant, +) -> None: + """Test snapshot service with the webrtc provider.""" + await async_setup_component(hass, "camera", {}) + await hass.async_block_till_done() + unsub = await _register_test_webrtc_provider(hass) + camera_obj = get_camera_from_entity_id(hass, "camera.demo_camera") + assert camera_obj._webrtc_provider + + with ( + patch.object(camera_obj, "use_stream_for_stills", return_value=True), + patch("homeassistant.components.camera.open"), + patch.object( + camera_obj._webrtc_provider, + "async_get_image", + wraps=camera_obj._webrtc_provider.async_get_image, + ) as webrtc_get_image_mock, + patch.object(camera_obj, "stream", AsyncMock()) as stream_mock, + patch( + "homeassistant.components.camera.os.makedirs", + ), + patch.object(hass.config, "is_allowed_path", return_value=True), + ): + # WebRTC is not supporting get_image and the default implementation returns None + await hass.services.async_call( + camera.DOMAIN, + camera.SERVICE_SNAPSHOT, + { + ATTR_ENTITY_ID: camera_obj.entity_id, + camera.ATTR_FILENAME: "/test/snapshot.jpg", + }, + blocking=True, + ) + stream_mock.async_get_image.assert_called_once() + webrtc_get_image_mock.assert_called_once_with( + camera_obj, width=None, height=None + ) + + webrtc_get_image_mock.reset_mock() + stream_mock.reset_mock() + + # Now provider supports get_image + webrtc_get_image_mock.return_value = b"Images bytes" + await hass.services.async_call( + camera.DOMAIN, + camera.SERVICE_SNAPSHOT, + { + ATTR_ENTITY_ID: camera_obj.entity_id, + camera.ATTR_FILENAME: "/test/snapshot.jpg", + }, + blocking=True, + ) + stream_mock.async_get_image.assert_not_called() + webrtc_get_image_mock.assert_called_once_with( + camera_obj, width=None, height=None + ) + + # Deregister provider + unsub() + await hass.async_block_till_done() + assert camera_obj._webrtc_provider is None + webrtc_get_image_mock.reset_mock() + stream_mock.reset_mock() + + await hass.services.async_call( + camera.DOMAIN, + camera.SERVICE_SNAPSHOT, + { + ATTR_ENTITY_ID: camera_obj.entity_id, + camera.ATTR_FILENAME: "/test/snapshot.jpg", + }, + blocking=True, + ) + stream_mock.async_get_image.assert_called_once() + webrtc_get_image_mock.assert_not_called() diff --git a/tests/components/feedreader/__init__.py b/tests/components/feedreader/__init__.py index cb017ed944d..9973741a8c3 100644 --- a/tests/components/feedreader/__init__.py +++ b/tests/components/feedreader/__init__.py @@ -7,13 +7,7 @@ from homeassistant.components.feedreader.const import CONF_MAX_ENTRIES, DOMAIN from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture - - -def load_fixture_bytes(src: str) -> bytes: - """Return byte stream of fixture.""" - feed_data = load_fixture(src, DOMAIN) - return bytes(feed_data, "utf-8") +from tests.common import MockConfigEntry def create_mock_entry( diff --git a/tests/components/feedreader/conftest.py b/tests/components/feedreader/conftest.py index 1e7d50c3835..296d345cca7 100644 --- a/tests/components/feedreader/conftest.py +++ b/tests/components/feedreader/conftest.py @@ -2,78 +2,77 @@ import pytest +from homeassistant.components.feedreader.const import DOMAIN from homeassistant.components.feedreader.coordinator import EVENT_FEEDREADER from homeassistant.core import Event, HomeAssistant -from . import load_fixture_bytes - -from tests.common import async_capture_events +from tests.common import async_capture_events, load_fixture_bytes @pytest.fixture(name="feed_one_event") def fixture_feed_one_event(hass: HomeAssistant) -> bytes: """Load test feed data for one event.""" - return load_fixture_bytes("feedreader.xml") + return load_fixture_bytes("feedreader.xml", DOMAIN) @pytest.fixture(name="feed_two_event") def fixture_feed_two_events(hass: HomeAssistant) -> bytes: """Load test feed data for two event.""" - return load_fixture_bytes("feedreader1.xml") + return load_fixture_bytes("feedreader1.xml", DOMAIN) @pytest.fixture(name="feed_21_events") def fixture_feed_21_events(hass: HomeAssistant) -> bytes: """Load test feed data for twenty one events.""" - return load_fixture_bytes("feedreader2.xml") + return load_fixture_bytes("feedreader2.xml", DOMAIN) @pytest.fixture(name="feed_three_events") def fixture_feed_three_events(hass: HomeAssistant) -> bytes: """Load test feed data for three events.""" - return load_fixture_bytes("feedreader3.xml") + return load_fixture_bytes("feedreader3.xml", DOMAIN) @pytest.fixture(name="feed_four_events") def fixture_feed_four_events(hass: HomeAssistant) -> bytes: """Load test feed data for three events.""" - return load_fixture_bytes("feedreader4.xml") + return load_fixture_bytes("feedreader4.xml", DOMAIN) @pytest.fixture(name="feed_atom_event") def fixture_feed_atom_event(hass: HomeAssistant) -> bytes: """Load test feed data for atom event.""" - return load_fixture_bytes("feedreader5.xml") + return load_fixture_bytes("feedreader5.xml", DOMAIN) @pytest.fixture(name="feed_identically_timed_events") def fixture_feed_identically_timed_events(hass: HomeAssistant) -> bytes: """Load test feed data for two events published at the exact same time.""" - return load_fixture_bytes("feedreader6.xml") + return load_fixture_bytes("feedreader6.xml", DOMAIN) @pytest.fixture(name="feed_without_items") def fixture_feed_without_items(hass: HomeAssistant) -> bytes: """Load test feed without any items.""" - return load_fixture_bytes("feedreader7.xml") + return load_fixture_bytes("feedreader7.xml", DOMAIN) @pytest.fixture(name="feed_only_summary") def fixture_feed_only_summary(hass: HomeAssistant) -> bytes: """Load test feed data with one event containing only a summary, no content.""" - return load_fixture_bytes("feedreader8.xml") + return load_fixture_bytes("feedreader8.xml", DOMAIN) @pytest.fixture(name="feed_htmlentities") def fixture_feed_htmlentities(hass: HomeAssistant) -> bytes: """Load test feed data with HTML Entities.""" - return load_fixture_bytes("feedreader9.xml") + return load_fixture_bytes("feedreader9.xml", DOMAIN) @pytest.fixture(name="feed_atom_htmlentities") def fixture_feed_atom_htmlentities(hass: HomeAssistant) -> bytes: """Load test ATOM feed data with HTML Entities.""" - return load_fixture_bytes("feedreader10.xml") + return load_fixture_bytes("feedreader10.xml", DOMAIN) @pytest.fixture(name="events") diff --git a/tests/components/go2rtc/__init__.py b/tests/components/go2rtc/__init__.py index 0971541efa5..26a8c467c0d 100644 --- a/tests/components/go2rtc/__init__.py +++ b/tests/components/go2rtc/__init__.py @@ -1 +1,32 @@ """Go2rtc tests.""" + +from homeassistant.components.camera import Camera, CameraEntityFeature + + +class MockCamera(Camera): + """Mock Camera Entity.""" + + _attr_name = "Test" + _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM + + def __init__(self) -> None: + """Initialize the mock entity.""" + super().__init__() + self._stream_source: str | None = "rtsp://stream" + + def set_stream_source(self, stream_source: str | None) -> None: + """Set the stream source.""" + self._stream_source = stream_source + + async def stream_source(self) -> str | None: + """Return the source of the stream. + + This is used by cameras with CameraEntityFeature.STREAM + and StreamType.HLS. + """ + return self._stream_source + + @property + def use_stream_for_stills(self) -> bool: + """Always use the RTSP stream to generate snapshots.""" + return True diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index abb139b89bf..bd6d3841dad 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -7,8 +7,24 @@ from awesomeversion import AwesomeVersion from go2rtc_client.rest import _StreamClient, _WebRTCClient import pytest -from homeassistant.components.go2rtc.const import RECOMMENDED_VERSION +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.go2rtc.const import DOMAIN, RECOMMENDED_VERSION from homeassistant.components.go2rtc.server import Server +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import MockCamera + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, + setup_test_component_platform, +) GO2RTC_PATH = "homeassistant.components.go2rtc" @@ -18,7 +34,7 @@ def rest_client() -> Generator[AsyncMock]: """Mock a go2rtc rest client.""" with ( patch( - "homeassistant.components.go2rtc.Go2RtcRestClient", + "homeassistant.components.go2rtc.Go2RtcRestClient", autospec=True ) as mock_client, patch("homeassistant.components.go2rtc.server.Go2RtcRestClient", mock_client), ): @@ -94,3 +110,104 @@ def server(server_start: AsyncMock, server_stop: AsyncMock) -> Generator[AsyncMo """Mock a go2rtc server.""" with patch(f"{GO2RTC_PATH}.Server", wraps=Server) as mock_server: yield mock_server + + +@pytest.fixture(name="is_docker_env") +def is_docker_env_fixture() -> bool: + """Fixture to provide is_docker_env return value.""" + return True + + +@pytest.fixture +def mock_is_docker_env(is_docker_env: bool) -> Generator[Mock]: + """Mock is_docker_env.""" + with patch( + "homeassistant.components.go2rtc.is_docker_env", + return_value=is_docker_env, + ) as mock_is_docker_env: + yield mock_is_docker_env + + +@pytest.fixture(name="go2rtc_binary") +def go2rtc_binary_fixture() -> str: + """Fixture to provide go2rtc binary name.""" + return "/usr/bin/go2rtc" + + +@pytest.fixture +def mock_get_binary(go2rtc_binary: str) -> Generator[Mock]: + """Mock _get_binary.""" + with patch( + "homeassistant.components.go2rtc.shutil.which", + return_value=go2rtc_binary, + ) as mock_which: + yield mock_which + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + rest_client: AsyncMock, + mock_is_docker_env: Generator[Mock], + mock_get_binary: Generator[Mock], + server: Mock, +) -> None: + """Initialize the go2rtc integration.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + +TEST_DOMAIN = "test" + + +@pytest.fixture +def integration_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Test mock config entry.""" + entry = MockConfigEntry(domain=TEST_DOMAIN) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def init_test_integration( + hass: HomeAssistant, + integration_config_entry: ConfigEntry, +) -> MockCamera: + """Initialize components.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.CAMERA] + ) + return True + + async def async_unload_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload test config entry.""" + await hass.config_entries.async_forward_entry_unload( + config_entry, Platform.CAMERA + ) + return True + + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + async_unload_entry=async_unload_entry_init, + ), + ) + test_camera = MockCamera() + setup_test_component_platform( + hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True + ) + mock_platform(hass, f"{TEST_DOMAIN}.config_flow", Mock()) + + with mock_config_flow(TEST_DOMAIN, ConfigFlow): + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + return test_camera diff --git a/tests/components/go2rtc/fixtures/snapshot.jpg b/tests/components/go2rtc/fixtures/snapshot.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d8bf2053caffd68f833a923ecaab07c5dfef6057 GIT binary patch literal 293320 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�&H=8O0^NrXTTi~QHeTq&?GNe) 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

=~_I`d*L28}oNh6xib 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ⅇo^gtAK3l~)mFi%&Pw55Ecz?{$U(B0Umv}_wQ17yC>0~ebz(1!i!?2^>jgf% zr!Q{8`sL*^x36oW=Wt5hG2rJj{Jldz>Vf?Uy_lzunVIuHHr(-jxVW-*V|Kn_(mTtA zdKr5+YnZ(JeI(4`O~In5`pP7srON*qdK<65XDqm`xhF#H_h&_yl=fK<3Jv^{Kg>&#S+_vvZA&mQ@N=T%!C^i{PvQe_mD z|CrSz&5xt{Kf{i_BCeAocipbxc&~eT-+D`F7ik$L69q$tjLPzY8)dzR-UjOVJ^cIn zxzfc`F-9j^>)e|hUq|RlTV3OwlXGzOuh|^`8SaF{uDpEu@^lADt4|LOygMDgr{aQX zC6ndDi~F`E@EA5ZyD75dGPCRjcPpq|W<;Sl^ zN7Q;1+WWo;%W0zUwHpMzhc9x zf0-RuU3Z^nw2ZYd&|iN~X#2b~u5*>8r`FZ^$aZJ%{JbVAw!F*Z)2xF^pJELyo~!Fy zGRXcZZ~D0Brg6*C*AGs`YVHS^*467Y@0nPH-AU+|-QD(K8GA5G+6~XYNmDQEe;3lZd-c(o3vX|k8@Ei_ z>ds@6n)*N4Lb6vUzC7)`_I2+grX3TF`2K3-Kj)K)c2{|QR&fpc;$yGmE0h+^|IT#w z(uIVwv>PFh9h0ZmZi%!i3gEJr23;v%D=U52 zhO2$1Q1P1Q)4BJ)IC<=jsA-{7o#89iSP6yi<@`T;m{M=C-;?M!c)w=#@tT^de3l8v z8SKxoFW3|JIa%_MPaMD3)S0XOntFK?7pBchoT$aFdCKBg5JyJL?u=P&+pVi-nDn-3 z#<9zrv+b_U)>K<3H8=E-Tga`&Yfn$>@3{NOqGy_L{y7#~u8#*bgLiyMnAzd@aQ`{E zB8!+s886-(a}Pdt{P26Loq4hPt8X{W^T=Gh=JST^Rqt2c<6EQgqtbE*yL#ez>sKj` zEVrM9t+@HIL(qA`;pJtqZ;g^8A|0H~weHRS`zT}eJCV4vG7lPqHomNRB`Xs}x!o01)ARFhcRyvO(VzbBlJn?9?t_-yXmcsnBG)%?V^D89)i0*cN` z6?30W>!~WJ{wGuZ%c^8EO^=Q+qZ8M%QE}D7VqMlteZ|709 zP@kK7_Of1@_jvJT2FW!u-W*+EbB}T9f#7{b)(7`UU-P%)O}=1w^Vl3$HqFg@z&RDDm38<$Pb8 z%u3qA=Xbx~QPSY?pP~9-&E~piPhx|m^cTr~lc>%q?R4XvBll>N!+!?j&YMS`J-wYW zdF~8>s}@Tx6g(9SPx}|mvcKX&Y|)#{?rS^pgoNi`%RaZbYW*>nI61z|hQ-J4xiZE) zSoH7Toy)>49C}O79&kK-Yd)KZ+k`rX!m9yg;SvF&XA8A+?iMgC3_9m_Hf9x5+m@Q7 zYhP7wJz(P4FlX(C>CJwPf0GwgW?30KD>Dh%8ED5x@JLom%TGLZU`r2Qml>o%Jf!!J_TcT(`OzD!d<3t9Lnm7V5q5ebUbslX9-Y zopW|5{XN~KASNX7E|j^>ZgbPsZT{6pIU5T4cD;AAak;SO`*gu@nN7)GH%#1=diNLi zl3&NREjs?*Wa6U#4ELY2xiD$Ol*6r%w;(Jn_S5638 z%raV&Z~ye9T~%l0m!lWB&3~<9(Yz}jTjY1gtk<|b`m;c9bCtQxW7bzs|1;R#$xQ3| z@|pAH=e5GqlEUNsR`>n3-Yyt=;3xaTwH8~~-TPU1iD_!&9)WnB_l5ViSMk&(zdW?z z@$NO^j9=c1-IF|d;bZf9=lo-AVM`AN?6!Vb9QeX5`B2ZT;K;m>;t}atcJ_*eat)a? zR;;*FVj?fQ-(uN!s~s0*0^1)X`foFp@DAw8^W$5;Gd4Fhc<+KpLx;jB-K-BVbgAHo#+I=XJ-%EWe?86CAB|Lu_WdSH7n|AkD< z+L`NJcJSKd+~ZQa5F;V*Z})$O!YdA|8_GXCXOjPLxFn{CrMx3O`#2lh?OFWUJ#YN~ zt#fsM{G4SKm-hPQGIB0Qe(trDePF!uTF$wfJNG`Cy{Ei+Z|c#R2QR#R_b5v9a`(#Z z&hhs`_%@|;RTX#L;of*?0SCW$55MKytul9C&V9sSZN$vHyrS;#EStw2@dhEUB?>GG zEAO#9_>=bK>!J6bo9FR~E$1oT*}QJUq+6+?%d3y(e?GS_tw*AWFMb!(vLwZ_TfRn#-H=wnZ=23?tPcGn0rOdPPWs+fBKI( zyt)0ZHlguySc(NxeY3E~lS*$rRkH?Z*?%|h7C+-D%X^==x>?HV$?VZ9ZH{-WtQdM^4=oDlG8Sioq72d-}C#A zgo~VhO7fhv-`}GLcx_*&R~^YJ<=xSu z&L5k!pv8#u?~WsEeNU@i|2!%5G5X#|KF0eGV_I*0*q47tV#?lk#cMv?3ivEp&Knyp z5`WVuz<>L;GpYwlzkaEC^w8Hz)XwO5(1C1r!+_n_=AHYPTWhiE&*$xu3s*TZ{Voc4 zbDvqIhb-PvCb^u}0DaxWIcv;Ko_oq^t{9 zcJat=JM}_w&VKhj3kCNTGTyz??`MB$S4DR3)ESc_nCk+r9^LxoKt+s=4a4E>+AFp! zm?6CPlB~s)8~+)2R%Mm`eaaF1+@$=O(ue44e1f|Utu(Fa{Fk~R<}oYx&tp-EH~ann zGdwtO&vH-OR|DT{?hU!TI~5eIk7!^2=Qs1<@1yPsyAC$wK94Z9%6C8Uy)bE(d5=`* z@_)}Yo&Ph)+%|eIoi1r{pyuqh((@H3bS_;#ETF`0vHRZ7>a#0(OtSNyJI~6Iaj}xK zKddSucs?yG^J1Oh6vzJz$3B|!PcE0OsQdKehWwuV!e`If7bUgM`7>SUX>UutWm?nC zPnHK%*>9~YztVNm@anQH|JGJK6T>UVvd1?`whe>AQa#DRnP8ptsSh zZKnbK!r6yAOr- zSlOJ4H4iIPo5NM|ynJRMo3a$&uT!_(@?X3>oN~#j`>NrFC0td0?1D2lEGiLbQ!+Wx z{IXv?r@+CqxM+t^!9U~Z&EgC0rLxaRa){n*u}dsVHX*!5`q6#O=8&a71z$aQ?Be3% zFEfW%^307~>5q-P3Z19tE1fj8S*Ezabkhz6iH7UekEI+Qlphnw6mGszKBt3M=NOk? z+|Ht%muCGcY>^EpZnC;ByOYOf@jA7?$FF98vU#MiZKv89hVgo!#97NW6r!cwdvxHws&C;*E8PFe{HV2CHeg^rG^Vp zr&ru6{+x42xaE1@@%Jp7*O&aAq3OBYsIkIOF*B~ZvQU~mf9|a3oY9>Yrz)Mx?^rcD z3jNJFxg>nbuOG)Bc+K$fGCV)y^l`5DmRH)(8O&}!z2|NR=gR%>g)*L)b4l;s_`A@y z+Pt}{DtnE~Y>|J3PMqv>&$<+FsE6|Sn(`F9e(;*zU14%l633R;d#z+NFTCKLE)clH zh4bg}n+ft~55D{{Wo~J~U9;@%pF3RgLJOt3A3X55F~PR9Un5IjyK_Dl zlM*O*CP?!8?AvyQR^=@oTu+@QUQKQ&DV*$AaI2XAnpMB$IX3lUjuj^^u)MdLeIWP5 zjODzRncE&U^jm&EA(Z!2#^AjegTRZr6Ayy489O9>-Q_qn><;xC}p;N zunC#Vm{rH~IbX8n_V@lP49>YMo0zgaW?t&G^Ng(j=J1u_3t9EmbULH^tP@=SYIdk3 zMlOBdBe1Ak-9LHq6p5lgPj03?k4d<=SNL&HhHtjZ&*wc0y)FcK6wHrc&J;9oToTl{ zn1!h;>_ME%{o64~N@nZYcn|KYEQmb%zG892?6SU(h0ZA<=O4d3Tz6|p{;^x%Ic}Ao zI5(L$V^*hS_H@Q--ka}OFJJr5(3`JvrS$mniejmPx_2eh3fCqV-df?;JaGqyUDv5? zZ0(vIHtpGuo3>2o>N~^v^PX8^9CO;u#1lLEJ^G$4D4v|7_dRUhy=Ml3A7Vdtq%>O{ z-<{U=y|wXG|MeZ8dOt;8e#ghncKEAxojAMLrm06}Xk2pZ`F)CQUC8~^`bw?L?LX=x zSng&n{*q*JOXt_`W2|4MufC>X>ia7>ZJ}90*gDx4wKF){4)yJMdQGp7ot^ce?}t0m zJ=M?Dloq_(nU(bwd;sW%F;-hBPq{Y}VH zbK~`+me+J%9RKn2_lX6ud*1z;v1G}!*6e*gj%%JTw+=hwxIe^n;*K!M$W<0v_xM^CIRbAm1_DZJmbm4^B=-Ioao;FSRb>Ct^S6PfxSJ3*V zXS*2h+kaMCV|MIxV>|EVmKobB=X^dM;li=_MoLar*zxI~r}OnhEB-pR#NE;)_4j{< z+y5Eb<10kfK9?|f%FJd|J3N_T$1JP422@;hOJtmXVYmp$9gOxl#d zk+pEXs9%4do{D#G;&s1&JbeGYer`AY`|OL?gQ7S4{n^a-pSHPscFp>i`_g&LLM>;W zEwi-q5!)y5ZD(@1^S7i8Q$D@!iCfHTNBs_2SnwHAG6Qv(`)Q;>a`zsOq*Z$!An9>$M_xR0RcmL>H6s3wk?6SOU zu6T`cj(zXSsTGqh^x7Vr!}@m73p;1N#?_lc)fm@n?2p>9!B=jD^1VF$3+vyXSrO=) z{ZpSWs31K4-20iVl{>$)I#|eB9}IYLsBY^$?!`;2_#`>lx<09FkCC^K)Z_iQw`Rt} zQ)MNkYtk}j+q-4nTemM#jdNiW@Avix`?m|M^xmRv9sEOvH!s>Vv*yQ7Sz-dv1aA28!-NA-%2P46?0Tkkq%xbgmqkM|s;OTSw^7vki7 z`QgnWMzaS7Yqc&f|2})d_GZ>qU#31k_c6h^cdulypL6hxx;syg%hVUEK6vX@Qz+xf zckhmX>*fiUzgT>-sAbm_Vr7<{=apxD%~7E2(yc#98MZ5GQf>;Cyl;Lk7sBHoFXJQ< zx^MT41SX+YhKp-hKQZq+HvP3g`r*xG?0IgTCm6RpOI#vY6?v`5>B}p7H_-*G58s|( zXTSC8wc5g)J|RNxH}}+RxzWFw>)=9e$)qEfBZOA6{Cdy5Nlc!d%U4=?I-9|bSso|P zMWpn{&b4SQ^Je?cU|}J+`=;T|o0n>m#PpfZp7_G;`P0yM-Q?YSC)Za*UzmI6jtKMQ zD@Q_~zwS+#cJK0@mfb6NXuQ^%c|@hJwDE=KUmvThv;H$o=6s{xE3U9==1v3UW1A;* zzc`zIHg&zkbboR0``3y@g5>w=NmWbTD_ZvAlKmb-FrCaK$1PY#@Uy?xI<)7ZBM&hK*<|8*`~?wFp{xI;Gb?C+ zG+39b7^lanEx@*&ufh5h;|j4;x2ua&)4m*KH8t2JJvFpRHlXKlhVEo{&Wr`zzjzu~ z_bpZ9uDYBv+T{Bcm)K!A6e+JA=oM!R(6CtqeOEMUnsy~fsk=AP@+%}hD-^j>7!$$gcPWnQ+~ zqB@+bf66t1o9n-er@E_sJ-2DC)Xg;#2D8~;9B@AQ{$fJ|(*uofir3H1dDOwFw&ZWh z#+7MqTC00s91U7_UXA6EtL)Yv$N6Ul?cD2L?cE)!UH$uBPQ_Do+1CPl=C)Uqz0Wi6 z?%xpWUL}0yvdH_Y-|I~E?JwJHdfBu4_Kw{rj!pc}@OiSpE_SxhZc?k_I39gv&~?4c zf1g!wQ}n@xkLRAdr{6hnc+u@@gDBV14UZq3U4EgUaOczo?2NrfQyQO#-nhdO)3(1) z`1@?>B>#IMs($n1c&>VUEo8abeu+y;NIcrEFe7}5z~wy#E4*^l9WSn7C|Oan<$7f4 zv*(Iy|1-26IOi$br?jy|a9+)1=^C%)2_I%8lxn-L`~K^o*^}zEPgHO5a@@T0>(G+F z`!*~+)V15kcislBYxnD_jyl9Ie)K3Y*zZ!G`r2I&^bdAxO}bYWbFIOb>++A&Z7t!B ziO24!^f4Fn&MT?>d3cVqlf!buAMeE`yok&D&mi$GLw9z&#A4?QoFJJu<2xc!p8eJ=Axf!n=D zkHa)dwI{V^P5t~{^oLz(gH~*{oSo0Zb+Kz+AC~=naiv;LuDI{&j)0Q?49rp+n)mzl z#N2!)6^p=hk~^)`sTC zS^wP;RnmN3aIPV6zP_YbcXix{0(W=TBlj17pEW(c_x;YU84+u5{AY+1x-e^gZPM+H z_hsKlI7#1ISpDGi1i{No{62R$%=l#Vr*3oBUAG+zILelvE0}b5hSaA$Yz&-h1&Yct znqI%vi@m)6bYc43J0BPA?+KCHFZf+(Lc;cUpS!1p+@6~8{M)OC3mk6k(n#@e-)gAq z@K14e)&{eJHg z`8{QGUzNrl?#U3$zP)B~fC<-~-ns`q^A2pWO+Kpn_{a7x)6ylApLYH2UUfK@&$2yN zT}|`W!Vk7P8G_gMpYD+e<}uxR?8A+;iIzwG(;olx`*E~ArntKH*ddh_DQVL-@6lPV zD|Nd^#pU_pWo%PeAD!<1a!YyAW#^hDM{@rdvAif(>~T+g=E&`IJi=#L`+H60Pyaqx zwPn3I;@4l=y!givbHn@pSSpXWbDPhZ_enXq`k+(%;l94P>-p_1t#0HRsqSmzJy>zO zVZ**n60YlQ5?NLs`)zM7RP^4gY02(u%unz6nG{}Qym`xBE3JWjbzuP~&tk{(_58xj zxf@=^emkIB`u;i_YpPa80dJh}n|V(+S?#LZ zL8ba$&hF4vJNbSex_l;TLDTdNM_L(5yG|7En`ia&fbUKJ=GO|f996&fEU4{&Jo9VU zh4hj``V9+KK5p0@{^Zc?CF_m4FW(c%%IB5~zY`j+5gx_&K_uM%!MU90vicv@=I?oU zwsCVz_&h_lmUpitOnw#K*&z`2Qf``_$=r#m{|s&w2b32HNlcn z(|cSyOa9EYFk5nOe?>ie{=sD%b}n5M8|eI=Ps2{-!Lzg1xo+xpPVD4&WUF7p8zOjZ zt+0uxllUUZ2468LzSLLGL+qzEteTSj%q-c3wc%ynhNo@{h1&%)pCmM0Q(87>k>K5@ zJBl7xn8+Bf5t%&uOlNE3`S8aTbC3INUGL}n;(}9+&7JCap^bCuVjm?Z-)eT_UD&y? zCjL0fAI5k@v3{z2Wak71N7Eh-d*0V(Q!boHTakU7y-!z4OwBX$!X)G6 z>=n8Co{g+gXV3lgHx7~M&EL1tZpzLlCZ{bkcUpW`ChykVZ#x#Q3Tj+w z9n-Mv=ZtQ5c2_kJ#p zk30*`O%0y^_IO0&!Oc&PPu{lPzM?QHxhYnoWaf$d*wfRTUdTAHPpoD+5PfaM^p8_? zcOIMYJ8pB$hXOWLR}Kl~to@?bG%|FHKTemmD-ciKro8%tjnR3YyNvtvlsY<3OqlUv zcID0m)nX@}=4jpD*tl&@T>jS%R+olgwp%LetZ&@!*(BW;<0fUfAn){#5>4Wp^6JsD3F>i2|hKJ_QEd)tJwwN|HUSMdC2n8{P_ zx@$}7S&kkB>uVw&6Jz7r>oy!n5N`}Uo^(TE>s-q#3@dCVb4@XL9h~;howq9FmiFd6 zSB0%_;!d|`Y*W4dTE6V)<>MC~9BA9|?FfiLz!Mip>xfj98Za%CvW5D-9~|D_Wip4>bdEuPiN)& z&wcPawc0<%yJqf38Qy!WX_sa_XM3{Lwrxx7gGG)@m(Ev@j8ff|k*37lJNLD})v2O( z!Qi011%el^+sHoVeU!5M#sT)d<|2EwIltM?_@bU8ZFtl)(cQRbskL$=hj50lqV zZN18YTN}P-K5sq3QC#sfrQTeG@!BVeY==LO-*BJz-ygYyeM#UY`F9cd^IRSteflPL z^%0qM)xn4N{<3K4F-ed&N?945a(+(MoiCT-s}70Cuu9+l+U2NzC$7I|eaLtJpM6WN zcK+3qP_~J!n{xEw@!Nka(x>X$&HZ37XKUX2MFxvYGq#>EScj`K^G?vLI=))W zLF!0q#kM;stgmM9UcR?azs**ZZAxh8vdg)+Td{)V^P9}6=aVv>x|dYqha_&z}zkkieOv`nVCw~3oQT<(PnNnCF&17Bv zxc6$|o;YQOz3wWv!j9ZqUg^lo=Vq$#c%jB)!E19ZP49(xoBx!%-MD?Ch-GmeS6l7- zAASbH%fr5j&d_q3=y+{MLBjUU`@Ys~Oymla=@+moa5+DD|MD+t!I!p_KlkqBkQV%P znSt@K{gVx8`}NK|ig}wOy4#Aul2>xYl=suDA4rw`XPDTfGWqe>$&sG>I=0{SF)P>n z8MQBZD#tzMzfYc5-#cenlo(}tULCixDzV$qDXt=m2;bRFP(ivF=F|;$xrsLskM;UJ9qIcm*wfzvNx~UTnJ4% zvQTcL@M|+Q5 z`?6EcbJ54Zx^~Nrtbu8#&uv%|SQ=Ke=(+ei(MHi7XV$X1`^tUH*^}vBtl;VXIL?yq zi0xyAZN8zi4v1Pj+_m2_qha>Jeak<;ES?bWXmfnG$^PVXW*5BTZWL*DpIfWCtUk~{e{V^q~A#W|`|7Qr(32g4V*ZSjVcHQ**$9N;zk1aFGxx9Hn>hy$Zd#3MT z>zxwvZ+p{HzRn#b!d$t#_!s)MKRC42uwP-$`ZjhYlMFdd?LNlE!EdH7HhC4CvfS}^ zk+Gj*X{zR7-+&H{a_(7={FDAOSTuB5`&wNH=jZ?|z>h+rF=`rS?l0IHtx4kdR?ds#m^?|CPSAHB3wQJ)I&oKXdPf>H>_j`si zhht0beNg=L;&wlyMuX?oJTZRRd)3F={>+(TclgYGjjI)wTTHUc9S%B8wA`h>F1E~M z_d3;;%&yJP`b?NvuP_{(ExS3p`J8yS{nWFp855p7JIJfA$K%MyQzCqrFY%@HnsTnK z5>iX!UF&2_EKC9tdG6Ts%KlAW<9crWmxoz_rRUBYgkGK?7x7ShrOBVy67J!1AMQ|J zc$>#I?DF%AG1*i1S}&+dea^9@%GW2d@{veT#!bFyF+3M<^G>la`;#{D;yI3VwVTV; zqfR?LIP&&gfbNUp;LPmT0*nIw^OohCo~t==oO$bkYnz19)~SDOZv6NyN##fR1QYXp zJ$E`vmd&}fWXHS9$MaZo?mfHwd6sx@`85-s`ro~&m*Rp=zI^R4i{Edw_LCa-(U%sF zYaV^m+jXcTuIZf2)OWv(6JLG&^XNNY>CzL7XI}eXIojSfZ%W}&-Z!?T=Ug_YM0=Md zy*jq_@U*OhCvQJ3ERkK8Cl%D~xJ_)~qt%%YKYs4qxNK|nx($o=PI~a_g3$Y}FA{6r z-p%^A(P`bq{SO{UUt)aj$9bZtb^kH$!*cWf-91qG?s`4zF^>hNy!%c!EMkm5l%jL| zW&V60hE;**_3tr@7>4nmlYS*~tM2ve(>lIq?3h#EbxGg6w9lttdi(F=5$sWc_xD;X z5!@m_seWdH=>Dm5pOn}%e!egHwN!M=$DJ!&rm{J|RKiP1_IYjwLex8Q5q3*_`*0 z(PZD7BVOizpFVGTp8i~@zoxQ|O|eAahVcf&L(`l>?|BKaI|V;~&K4xlzU4o|8}r_p zk~IQ9cx%`q#PxpiX|pxxPj@+-)?B*2ue>$n`LdshvJ3g6H?vK%+WWmbucf0de92!) zrj@)A?qNIjRA|qcc~EBggc8%)EL-&&k6)j@b!J5=^FKN3ve$fCMsp2L&OiR(Am7WC zo2=vf;t%$As;FnI_Y?LNuHGhx=V zB;GYS=F9muDzWH&y*;t%!ZL&r^awUe*;jh=PQQ^?~yqr;{iFv#24!K?#oud|< zYxC|;*A5R{u4w2KYi*i6b*p@u8k2kv*X`)->pmUq-G2NzYw@jL*M-F&1;0Ppm?O92 zJ4=^OruRM1TfS?)u^y8}&XiGT_?U_sn71chwuO zi7ajN&VPPCCg*~b+w9-(<%CRTCA%DB@RM2R>9#xj7teF=$(8!DsTV&aB+R;5b|$Oz zy3>zG#Zyo1n!U5w$;-6U#Q9uy$c-7yCpJCUlE8Ge^m6+Ni|+QAxyi@YeRVSPHEeS( z{ba{Ko#Vuc(6>)dx#~7M{`}0@cduu|mYEvOF$o3-u2;0CM?7H4{Z%WY9cbS5Z0d5& zz`obQB`;)TnPbvx6IT6acqIFPO+Ds(9^+EMcY42$I9U51H0lU2x&3frzL(6JgF6h? zeRgbkU7K=qTX5i)(pFVdh1UXcmn%4$djIx(X3A@QuUz%Id7he(Rkoj+!;hBT)oDsc z1wY-say;Rh;O?r}-_5edof3PKDp<0;`)aE_}6{BX}|2fufY$2TS9&F6V&xPonUy!{-3D>sUD zJvYSuan3*7<0QJ~m)hcQPueWaUq(ot(`oy~9Fxt_C$}m5x&MqCR!ftA$jx8QoUqHE zy|s${bKfOJN7o>+ICW%bzQnn&lM!ecHS0&xC6q6lG<4R8OjI zvbs8%U7RgX@#l}qb5xF3$Aoc6OH5w*_vZS~KCy_>l+D(auJ_0HIzD`j=9j2N?k^+z zt!dBeHNo>Sy_0iC2@gqTT*8T===?anus#X7T(d`yMQElDS#U zzuHY)KDjLu5cG=!zX&c4lq`%LvG1gx4V^1n))bi>J$3tS9 z8;e=^y(d)7GI<}K+_`dN;zg#J8+aqHTLzmSIO>&Mz9;AcPeMZ=t907&#+v^OFZ=nz z`u9gy9SEOme?7kEyPxw-ljE#=Igi}atcxp<+}0=a=}}ujf=&5V0L#=PO* z{hy)gMDwwSpcBM5|7Vbxdp-1EP0@+mb32{~JrUczPr4-Kts2h-iCq8R*A!kayyow^ za$QW)>XMiL4D8l@mD_!M+6Tt+o`_kiB&Hp*`p>XV^zz!%Q?D!byRO$hTc8spb>M)& zd$v$JKK%t34OcC*zb(XT-)?>4(B{B9bL}4GTW>`QtU|MB%#@u5Z_# zaa*@zyIAk^irnh&6)g^NzNyvGB0Iz~R!8OCSyp!W@Sn#G<#IeuQf7yf9T!T6r^$Zc zwS8*&n%R_ny-dUsdAn2}DZ9^Yi=<^7AH<1^)jivAU~a0Z?U5t1>MgXSOV=!r@tV)3 z*5JJF#v=jt*+*qNkNZh^pNRAQZsMr)`sL@z>tjCeKitr{Az~)W74_&Nm-aX_aOo*L zYxoh&l(Q+sH+v1=me1xgvUB)ld>yW28qbz)K3f!_H^0`B$&_!#+wx+Gx9^`EY7X0B zeR!pc+8x_uhYcIk{_*r^wMHNN_H_g}%3*qW~mt1Emz z)zw=XHNCB>j@fppcXipHn1$0n1pN~@5Vn(9_u;=gYuJ;T4n8X^I+wg-#q!63OpMu& zT@$`ateyU#=CiFqR)g^3Lvp-TXYWmFkba!BXI>(QyUNSmc601LNFB88n``psF~cs_ zr2;Sh?423^FH6pD8vi+ugHpfppO<$|Pki3~`8wyHljrZ+Z4fjQ5Keq~d%|<>dWn|U zh&d16i7i~S%y>fWMH#1MecSvCIHi2wKT6`h?eJ7mWYq#Mt?yrrTwTJud|sYh{Q2&$ zV_cbMX3I#d&Ro7^*EOTmY{v%$lYgC?FgxVoLcKdPxC8iNvt^!`Y`MLo&TtBI=2nfb z&m+Z(-tg_JEwnjti$!lL=N{e-7xE51{&=(H086mLj)c&aEWgd9r zy*}v4r(680ZM`jQ&orjjc{Zl~+L^+nkHcKOfR@^USwbSA;B*_rYJ-C+=h+b zE7{LQnF!1+ddP5NPobQ_mNj0^3#La{Xy+d_(!1*XpyFtT@TKi@9D8mtzn*x_P~CUF zynW>^-rYenIE8Yq%CIc<{&1w@P}}dhNmB)H@3y>B)OhZ0T`}I9%EmSCHHO_lfs&x(|J~@r=9h=;BbF&|Hh-;-q&-U-V0&f^JqEmuHA-H zjrK<#lJ-at2r#KxCc5zMhLjCI_dZ9fy|QmN5Uqws3M#m!0K zqhP4(GW+_^4)&NWeUl%YU3hT)&Bj%QE~Qu6%5x10w!3*q$v!-Mc-2%d!#ufNrq6#J zJ#MwEHU66E(ZzfA_Md&sre3Y%R`WOImHDnhf1R&4Urt%&cYbMnY?A+?fRDNJ^9&oh zu2fdo`vvUba^`p=zAhrbr9t^R@2|J5EVu55nBLEv;BKI*(dYBfC_&-wscG|!{>;1) zB(L1#l)dTok`r;PdKr#!dvZ?A3d;T=8#x%jZJO!`j99Yt&BO_*bRYqX_3~_UfB{KZ%5|^y3%{^m%_6CM8D*jm{ zbY=6TH#KT=l4SE^-KAz(WM7ycq5PU_eOz5p@2%|W$qeROMPocpi%MwsaB9tuYk02t z=IgHcezL~|y)yP+J<#T58a{cZfjdLouKjjtk45_UT`wo6D|cqR&2;BEc-NurX6$=L zlg4$`g_oxBeoQsJCVZqxWBrQ3pDloa{!fm%Y4H6@M+Opk~g|ABEtdCE+ROEb$ulaMwLd|=>Yff_BnQVUHQtq$(wDKj^y!V$!ZphodY0=jS z*Ey}zn`fD?DE-4HrPlbM^3}tf75z8&EGfPJ=^qC5Pe)>%+{5AY7lSXt)kg$gFL$!p z@gP&+sA4={nN;?q3-7kRlhSoQdvee2Uvp-&JN}xR6x7raa#hb%>o)W9_i+zp{em+R z8p6Ku-Cy0kkZtj5t=9~*Id00E8=9TEu4T-t93(q+%fVT7a_8nG%;SsU+q?IK&b)tK zh5M}%j>s~W=jeZVJlAHnnRUG12H9u3GtcjzJYU${@!9kAkKV2CN>1NTee>j;b6|LG z_>`)u!V7QnZ@diu+A#5qN=cc1n*@XL(a5>E%h($%;7Q?~*Fs zJTCjs&}O{pbL4?cBZY-a<2TK(P+DLe)RR+hnWB9#a^s%8;ZhBLK5y>nebZvgICMSX z!}V(Uo`8!J*bbeDji0<+5wHSo8Ad*~uHc4>=1hD7?z_v5k-QTki~=qK$V_|NZ+g zYo*!rfQQ?^cLyw-ZE|_bVa6LP zW6)ofQ8};k z(`JTA(C%4zk4v?KgnOj9x`Q|?XYk&tRsNDtTDDl2dBP3Nj4w$^3T~O34(aKIX11hf zE}!_|nBLQyEvEGnZ&%&+UiKiX-{#UIj{=*m)dk^t>P=<8WFIgJb;SQ}N@u#~QtNtb z>bEBDul;}9epcyUKBgP$T$atw<}T5&>q+tSgZb>iA}sSxMIU1pzklG*9LBY8JEBe> zKWFXEUn%)&pY*ry6MJ0`X{d|V?SE$M`tiQp2HuP!{=V|2SUI0uuMmc78yU>ZZ|?k; zAo}HM1(yCJr{3=tP=1Xq*w6x=9p4j~R zItuNUY>Yn(&92wnI@t2O!&d+Z!j*lEG^KI+(PUpJYV8>03m%Na~A zw!A*Shjl_#=M;g^bcWPl3vaYY?raql;+URptd^NC?%`x-X&vR9Ah}2QCr8G$xfbi+ z{Nc&oGR5C{0_#H?sYezKl4@Nky3&C+JlpO0*t|cbTsm~X&e%6R>N$thg~crli|3eJ z^uCppvl;cMhaZA{ybTMqew&Fis3)Q$3j2(h0UQ8W*N*m#`u1_fI`ky`#*J0 zqMVN1;%ohP?&Cs{Ty|;yC;ihzO74{Dw9Vf<{d>;>g^Y7~!eY1fZ~jsA$%{e1>gd#k zi{0dul|4VlY&6k58DBoFYlm}y{h6HX>{va%hCNNYHb-pXeX(PgS0~mkum%*R#sf;rKgo0wWX4rtN%FI7E*w z->m&)kC|S>YwfEk`l+}5izc};9m$(`; z^BTvb)xq{@{K|_n9+#{M{G?bt=h?S{xkzTRvy50PV({<1I{7iDx z@?BH2eX{aYUcH>haX%isFuA&F`ojZB{K^+B68pIv%l_fOx)~t`)cW;4~7jlR+#2GRXtoddy(i;|NiqkCK%ex zD*N|+ozV5Yq8D|3|J%Wzbm^ab!?p0(YqbxLNI7ut%5BpOSgJfrDym=jvzl&-sQTvb z$8229xAAMw5$&?P!xtmC_|@h7VulsGd7D4&bP_x8`|ELzu8y6#kC}@Y?$;LjEVsF@ zZJS@ww{X_azPzSaUPpXf*XPIgME{$&j!XU0zVH1=zH|61eES*=&An*4aPg0D^aM7QI!*VP$1Jzx1@Ms~(E z3Bi($_EE<|i)#h+8#e(l`2-egZw#-?w=(T|>U zM7=$KF0|OOr2qE0nCmxoPdR=eYv9jUw^;p5M3J z;2OToRQtn2%}oc3{v2PX@J200|0g?}+SDt%J?0$WJ9C5BnMOC6`SquHwN~-Y(m%IP zSi<-Suhy#lm8z1l$9L3a&gq)Jq))y5<-s>QE;q;i^RP&|!Kimm`OC+y2nV-COm5(p3Rx$e6i%Z$N|QrZ4+i(yUYEl zAg#?_A;RLrp1ucv)|iL}eYHt#c=4{?WdCPIEiQTGO@4EhT%X#ib9L{`gzO{zVGmi8 z8w7rN@A1E_@pAu^gbi+A&L=n46nMyPtuWZ5>~fDIjOBt!)t|>44VnHIU!^`5_Fk-- z@yIvjqFyw&8FR^nkiF^t2QQejJErYt``{|lZkyR zj`vTde{9ZVS+q5C@2^J>SiM*~1m1o*=H9jLMq<^vk4<+ilaF&>=CVFD$8lrK-lsW5 zD@_X4uhy6Knj^lYUg1T`ccJd(SN$jYly8~DY8knB`aPrT=C50yu%0cs+x^REhsWHH zZAG!14J@j6UdZ0>F|xIl(i2Q6lapzBY=EK~E)A^jQR@ZNrx-{vi%Olr$b=^H|6WF(0n!mj#({EqQ z{Mv#qpLuOudgomJ&(Pg>!CG}2M^4Gyy*sY?c-@;8etNswpYwg~lIyPuZCX2Xxl5MZ zol54`I@t#v*I$31@LIa=TzT)V8@qm2+Q%du_!*l&CvoZwiBt9b=RCW;?ABj%RM=WG zFI++Wh}h-HR$DRL7=*{V(kF=*pXk9vx zl6Pk7y~Y$Fh1LRQcfZ)0a~ni;|9M|?U~S-$TW=BHU|MEj^R)be^bV%{bt2DSbMiB6 z+i&!U>At^4Y z`6OBj9&hFM=b5!2L}Gb3U$5e9`<`CK4k-N-5;5CQ_^PUe%EADDHRq7I`VMxq-gC2LTY9*`Dtfj z1h;xlpET3)Fr(}8$43v;2;8!{J*z_Yv(9OuZ_X9}&KgD~Uw*u%Tw?*>VU~pZZTs)3 z^)5KwvhcBY1Cxe$$>BqD&Q4w)$#Bsow|K6$*yfKh6V?X&*%M=S(bQV-y}3>CzcY<5 za<=>J@0n4s`PZY=RWq&D?TmP>v+>x!XWS(>PyF5V^jr3sn0wh}&tIKkDxM#I?_;FH zijsE^j|eW1Tr+3dgowhW49iXwDxR2hA^qlC%M%MMF0b!1znAF~?C?UEp{seV(c>jw zg}W`&6bu%;zVQ7$SF=Y?$&R+$zhyKt%={LgJs^>_A%FePzTSztHs9(kO33KDqc1irT=m>In?son8*;`qbam-pJp~2~{ zM^3O#<+;oIq}(BRs&&zHF`wn~*_xTk4^}7Z)MOkALn=6VKq2?y6_jQxsIJy;-l*$mJsSWjW))fDp#j zc514}qL(^+ViV0-P}20lWb4Bi(Z*A^{(P9Z?Vbj2)c!N-oQG~W9(_Hz{h;6H5X~WDQ?!fO>4^Ay*-y$ z$Dew?r!NBB4>t9h7NKG(f` z(rXc2!Ccde&CLrhc5>)4y{!8yv+l>KhQ`d#vp&Wdu6phizHGgD&x9NSfuH;nOMfS6 zD0DTlkmpcTxJv z`Rprd_ze19N`EYSE!4E-;sO8Xf{x}akMG=AY5BKgnP+SnJLC7}pbyfoUH2WAh*J4C zlP6Gox`(#K0p;H^mJYeQ)pq1rNMzT{_j|wfrB&^j*HojK?sZ?= zRB~a@={dXd{Wd40XH`90;GbGrx#MK^eZwrxgo@aNd-vj|Jrb_w*X+{wOQ^lLL_NaL z%TNB>^TuP3m_l1-gkG4jU)SvChs3>#)4#c&Gc0d>dD>>`9n-Fad#iT5&|IOR793Hi zFq`|aU$e|&`}^(M3b}lmXJl5_&SWrO%wYbkZ{|DO?0xdj<#J`-HI%N2>Gk9&|B#%S z=_e7ZBW0b(5LadKUg+aQv#NWAUa@bd8MD3rS|M*Cd+hzsPVv;{-D~zOUOY{$FaF0< z)hm1Mo-<^gDC}*Lu)E)N$05djbFz1d{yvhCv(kj+-GjI}vSnY_yiHJ#wYDwqVk;G8 z+xB4Q{irPlIlWSweE08hTP)5S*O9^xnb(AM-&^(_{*)k6 zcz?_LFGXpmj&Q50t^UvOvQ6EAb63vg?;n=QO9qv3*&aJCKh1$k@zwlIXK zEB`(maM5k;OT$B3?|nViV)*x?N%x1US_zF#q44*P$6_||t*qVrapgp{2kz%&Yu|qF zu$XR|?!)r0XR=Cc=(??g@e5?Hw4xD1!yITKy zLVs&<>4FU79>y7xW=719?&XM1|Kj;~#}dbl^H>)CC`dk}a$f3{)B81QQcC4j#wG1B zRwjNu4rl*GD=*&pjuwr8u-$7=N{|uRJ8(md|zc;iw^fJELahCP|q_P^G8x058TkH3%HE#*_n-_kL z>w|fJ-m?9Xp~(peF>(xl9%?UT36`0wydz&Bsj&No`26kK70FV39@!r!{kjqGT6@l< zTOrT6%J^EMj>~aaZl3foc*1+R=)1=_Uw@sv;mOfVeTQp%?(f)rM?7|#jKZG}jn5w? z>E4+7;&{Gq}D1eF6;<+qV4Vx4o0>VKAlBLiqf} zu9jIQ1yStrw|TNxv!^iD$n4#rxnHRz=bt{~GcAX23s{=?h4`2mI67pbWQDKp+1|Kx z>8VNn=Vu&kag(OK`(C|+e;%*P%;!F~pY`5kChWRavVqCcN#HrO5%${Xm zf7eKC?R!~sZA-$cTN;x;R3$u0yxzjQS;YCh+?-=6)ujg~U2yhcx%A_x;FbXU*d4Zg z^);63O(#o-J)Rn5b%Eu+%raIE-L^{$d@Z%TeQuhEJIL%j`(XAK$s3bJrz)N*(QmI@ z#KC)W@2@)-TSR{KpTDPgn6Ir*x~V?DaB4{PF4Z!@rX|IB-83zi+VHu&80W%>1E0lu2c`Bq)tQp*?8Vd|iA z?Vgdq#poDY6D9T2lS<4sUP>~a(!7SKi|2Z9o2auf)a;t4}j2n62|K(ORpCZ(2ECSspj_vXI&RLc#9kIe`+^ zMW5I3G3+V5eEb;4+UOhqKAzI(xm|ek=e=O3^(FjkRx9ngxcnM}xZ=LdDO!wi{r;16 zowEYU-gnNA5PbK}W6`WP&Vq|3AC_ZFI`)OjHh%IN2D8Gd`QQ1x6Wv`MA6}o46Z2*6 z2g3z!XFlBeU9f#tkjwj@_7P0xI=f5$^6GgnEUcQnqAL7i|7@LKjF*2Tx3Qdurv>E~#EgiOE`5_VQQJq_^3A=L{zIPf1{3Yv7dCG5Iy$-w)cq zbC^Sf>%Q zc^nsAp8c9@)yie(J@9V$)+EzoF_l$Q$E9Sc9V$hqcan?)jm%?Q*~&`7r&mz zT*fd4*Lu=p+Y;M%7Ht_JOMN-tZ9KQ? zm-EVhdP2@Vrd3%M`@&W=vz=AyF-(oR@2>r(BI|SEgZ-B0<*vLi?`?XvFz!419N8V) z4wvcHJlxOBw_BX)&913$=FDiVytkoxxBS!7hNd$#j#y4GpWvhvu=`P)fIxHNlqV-P zd^sF^hBNF<=)3Rz7p!?p79Kyor|V$l+k3ktELJWo&Ha!ti$zhns(kOhr_7->%lqg4 z{H`V_pyk$nk$2ynkk*OCpVdED8Emi(UM2mjvf_}%Y%$(0JFfWV3IT&F4@}R@Et=6P zWUYU{e*R&L8;(`)k;p}KmMu39a!-?e1DHwSVCXNe};$}6^;z< za{>#Vhqp`@I}n~BbuZ(s`@yQC+e}!});>Qh$U6OD%JYiG1?%o6K4V^ai_J`{_j$zX zuone)9#?w!My*x(ae`~t*#_6X9Fd%x8Jw@r339M~j^asqBJQCvyZYFU0Miq4d7&R4 zZa%oZ(m2ktt9*~|KB=kKpB@THbuNx*&Up1lzAoWK*bnE+FHb!4(r5Oq(bE)IyNcnF zmEC&<%ObZoMZC&i+xXV9miftl$mR+UI@Vksd6Z+Ti+kXIh8eFHdTkG_@9t>TH{tPe z|MKsHDyngLe6_Op<6TF%JP5YH(I5+IIjEpb5lbRb&-?zQ zFeScq<)mG=UZtL6bhMdaUZPaGeY4VjwQZ}XrL0;u?_@{l+sBz5S4~+no?c~=^?me1 zcJ+2Q&0RO7=YIWpYO=Gx+V?+6i(>6A_x)#xm{(=(DR6v4uu}C}cP7?bI}d+4sPx_W z-1~JZEZniDcRa~HsrX_>=%US0oO;UHNA}hvn4W0#@>Kb3YB5FVghfoKgU{`Uw+kl8 z)pIHwvsG?C+b?i&6YuBFt_HP5TAR0@SaXb(L-6p6e~H^OaZo_{gyR0W`}xE^7_Z#D&8~$fYwMz%Yl=M%`G2Pz*1ug=62AC4mjsj4In%n@ zgg2KX{%OSblmvS7pZvQ)qG02K`vvvf6Xwqj_`SntaY1?b<%Zs`5f7F%o|Ip7{^G}m zi^2|d5B|+zk)5-^^TL_$4096_?lnwb%)WH}vpH#;KDu3YAJ%+sd)uP*=zZ-AALkd+ z!7+w zZI6{kb|(_sgCZ1ue@ob#!MgeF(e@ABg5I_=9&Ef*uOs{foy^&r2Hf)pxvH;h1Cp`Lg*ey=QI5HP}>z>Z+w;lXr{EQ*5p~BDTFP z?|J|9#}ivVYzdv4+WTS0{h+Q*^JM2Q%hoIj+H^(#wofjD=z`k^T5Kzv^B$JJxw-zF zWk{e2f8Nc~_=JN?FCW|J!^HYWVu8u-`_B?%rBA-TTav_ObN=Y_2)>86)~t#ydh)>} zZC9S}LSFIe_n(d)V$jljTd{uLgO**IlC#p;B^+2DsB+dnUp?)H`G&dg+qW;~S@%v| z|AW^x&9(?fspvl`x>{<_X04Bvd-8SjTxs$2lgHZ%GG<+J-uzo%a+^p~@2|h|uccl$ zZ{-*LYGH8CMfK_4zGv4XB=!nK@Tk1{+b3gsH_kNrd&~-pb-Ql=l~9@MWg!;&Sl8J_ zhd(Rn-m?cce#k79;wbgi(=^v!@^P83>5U)q$6WJ#jbk@H+OqDJe(_-$j-}E3izRd$ z%*s<_wg~CH-FjkeN2f}ko}sAt>e&&OE?#;3pW$FPgB8cM`SOW#q&<9?7ynFisI9Nq(T2SAHdC!53Z_jHlwP=cll0E%@iG*~7&x@V2_RQrKNae9fH%bgV@rGXpL;PJm#*HsSTx6-+kTJQjr;STCFm}=bx!JL&9=G89(H}& zClbGZY?x}xTiU>QlW7L)6AKQL_0gN>sBmy@$kaLNsZ+GzkK}%S&vnUeUz?a`Te!uk z1?&{CY5Ml-QQzGvj%4q9S9i|2qrQb{ZTZcgN3;}sFP^9~)OvS&dF;&PM>mO=UuR;u zv+wuH>jzsVhnn;}I5ElmD!7SspLW7^%dOH`gNVdd3!`@a0i= z^Zy*ZuvX~fw;6Bl3;4nf#p9{Oz6#3=P4V9kW*5-2DFY#Dgvw*U!sJ_&FPR zG<;$XIo_)x{kUN9sR>>ZJ7XBvuKi&s&dDvLf4cJ#$L4~UbCV`2biH@JDzonF#q~X1 zy946bdoHS1Z-4B6Ehl>Eg8=!-8opMX3f~JGc;e)F3hdrJxWb$DUSvhwh z(pFaM#*1g|_?AAMHLtm*C~ad#gM8xKM)g^Of74}|x`_#i#pgg^B#}Rv;usiafY#x@Ks4$OfKi{Y!vNr$f=GTRC zOfT4;pZ{ZV%|2)I^IgAZ#MKt^{K$S=uqL?4clA%9cZ}VCY&j$TGi;Y$bUFND<3B!L z>o|+KxpkXD)mBW9?%kI;@zG!AhrT<_jWe@szW+<@_fNJ$(;KQZ_P4cb>~sO z_N{WuH=iX8emnQ}yg$a7BKh#{=I>%Vt$qxW~LG*@h`G|99AlLul0d>pN*p77Ui-m)8`A0 zESvmz(Y?Fh_9k!A+mblJ`$GDm9cr29jYO^eL{D?7SMkgd7Fqg1W+$i6vvrS`vqt+` z9J|EmaB1B$wdFA54UMCNxs}8ud`etm+f8V=S2y(t}^AlyKOwZ z$>B}pFQ3HNl zwKqt<__59fzrJ@I8!}%%IWS8$_&>w*h&Mv3w@1Hr6O5No+!EuoP)u!?#On3!q3Z(0 zCtZK}Zl|_GtnA-qq0+9q`+W+kmAqniAJG-HsBCZUm~a2yx+s9X;dR|~Q)`X|mwU2Y z10S6guh{?K|>hP)RmOZMx3`+Sd(|9j>Wz`iu2)VKLpPs@8 z8z0||&O28hIXTgzJ^JiK3m&$n(!CGnyos&cZy5XfLP4mXY=`NA-lgkKPF7Srcl|k2 z@1EnumaZ-zZzXANskl9vVH>}WUe9%ng4&x=cXnaeo(`nK*joW{K5 z{`8r*+Z|7wzmYw^Xw|e+E&49Ugr~$#nlp!U-NmJRiTAiAdKLctEl`Z=O}~_|U}`MKN=rT26}P7S z%IQ~K)-x|gwS9Tg{E9c?==JJTe*zx&8gr!V`>_7F)G_4?T=k#tX%|kO(K_dhzzu0; z%OjZ?T5t8m-|XuN$4!RefaNd{;YMp6}5ADwt6X+%+oJlzno=>_pZ>J`}G*s zJ}gzb_~}NnHqW6075a1RWE#un-A%U-Y5H~e*EP{(rQfrr>nZE~6n&b%Bl*p|gw6ZU z@hPvo*!rOSTJV(188ZC~{s|Z_6m8k{#r_aap!3x2Hbv)W9npI2=<<~D%E@al>^89l zPd0WEYJ2)Ncf-d8;X!}Ct7uzoOWtw&PH6Np*>)4o>&P}VIvxBi^ z-QAzcmKAEgg@H*cPZ-v%t9^3VJoewrDzyiPKd%00u#IhrtK}N`%JWB`?dx#-I?X$9 z?&@bT&891g}JPX-n9LAEf?H=QR(e*ajx10@;~ij zI2Ikf{OO|USN80lySGa?72ZT%s7YG&jd$hAkKYPT2wAs&+`s$u8foV9sJOrnmlyvo^!U{mcx}(#N3VpFXI_>2*>T5K{&8Jt=i1z@i|=mwXAscu z-s2Gz{F&#zD_362{_aT%-E00;9Cg)en_V_%MoRy}{|xCfeQwM&f9k^$c72n2n3U-n z$8FzKV{iYiI%w^@H0pD?L&op^tVG}0!KJm2&%BtiJZ)dzuTzOx)4E*cei#G@hd5d? zzOjfmdfE8=kIjdFA32)KwjEggt&SsEWOe1G6;D4(oc`P|f7-Fi%E;dK(yrWDJ4&AK znGm_T&BU{nXYRyjoHHg|e0XeQ=WLF)rUQpsJVHfp`ARr$DNtwg5~=5nUil^ULYCIY zuM?736(8)ad2i9}ch~Uu(Xfto@kjsiDw34iMgNYF)7t~Sx~!GQ zL_9L@%;87~yf-`V;f?2thjiu6{l4awD&CwsgX8jvWy`l#A7Ra5^`9fe{Yhnc{l?85pPJZ0>#inp1M^q{(lV7H2%+;*OccGJ#c` zOG;mLe&>YE9Ubkp`W9*{JKpnTY>=HBeEib6JDXG|s$_laTcIfO-DCbW{gC}9*WYZL zrR{6>$gLVMwk*n|U*y}H)1rZ=SKs@b?8D)-vzBl5;mJ`y z&M#+w;*)WS;W^vp(6SjDUCK_qpTim=aLD40hT1a0#3b?Ge4AV**j!T6x+`-%Z*D?< zsnmn~<6qkoqGk&}wq(4iHT#bEdxqvGZl{Yk2sz{w*2_KWUNS|>!hX`#o7VdhT~sB% zU)BiC@G5-%Uev97`NAhVRAks{KJE!A5>q2{i?65IC0wAOzz|?ZR<|G>5o~$X6U_oq?a9G z>RPaF($7gIi8*}t6PvpyXz2Q~AN7%DwAX8zyw9iPFZc8FX9{!qu16W2bBW&{^WgUk zhbX6T|NNbYRxRkA`k|VCZW8a3H*z!NkC&6;5+J%7bR z3vJoUr@}`MY<|yZ-i@YugXh6^HX&oE7!-@16Tk zf+auOy!GqjpSa&Ma?87h&lj2=JmJo-&7UN1aG=S;_z^Gn#9a>FXG8yO=-ljbXW3Ti z9Z8H5R@SB)|L%BwA%yR136G{jX*pkb|K@2|z6!=)d6VSz*_83|mJiE!JUn__{XwIh zh5_5lg4BN^wsp-DYo@5x=r=x7_Fv(BlV$4t{T2oNTkbpf9}qp%$Qqa{()`rIOQoA> zuC0Yf_2K?aC4b)be2seHc3a?jzNDzwBQ7!L<2TnEzGHrp*?08wr|ANLW+7hu+o$ig zw8{`MvUj}wdK#Z9!(|o47lCppEFTl>XW_f#naEJMYcP=U7X*>zH=hG?&Yn~k-P6ql;3&V?1b&! z^2d%1mJcr9yBIHBa8hh;gvRY}JJlEP-kZD8d_m;iDZic;3b=?g#>Zse<5Kwa_V)=R z@tD&okDv20F)!Fqb&S(c)2xm2HtQAZLYvbI7iRn`lD&0I@YCZ6?|F;P*-P5)zV~&} zj8d_#{Sn`%Z7H|=*l>gSp?+m>o=jo-KkER}9tviaUN!{E*3APaAO=jjS% zU%tyTbH!Po-8spkjQi@rg~|?kTDPJ1Qd7!JF)H_v`$pvh(VdD14qUBy9IvLgHkaQ< zW~c6Ar4K8&D@tE(H#oHE>0k50>n4mEj*pUOEA;JRe!NEV?nB{ycOReRbvS$U=ZUJ< ziO!5HT^;Rp4`Q-D6n<=rytDu7p2w$jFId?Xq%!ZAWURC>aKGvK0B+8Wk<%XPT(59j z_H^zYyGy>4-rx6$ZtDNeCs4<;iRE?E*IA~~y!o>Zgy>Fq`6V%P>WMtHg$YxHLr+V5 z_|ue&CabL%|>Sz0xCw1Hbk-zONf_iRUXn3&2RABYOuSbM8T)SmEKO)lL z7_V#0heK;a{=@{xN?Y7c{e8}ay=P6U(}l+|-7lMDEv`l0JiNkMb4214=%YM z^L<)=Yr=O^rJW1+F-b67m|S()fN9;4M`tA(%s-lhr){!a@Y7uHgK}f6#cFxCr|NId zPN+~&U46c@YlVVv$1ndLbEn;EJqLH~uI-!jnz5jv?VQQiJq)uhXjSt?v`-J*S@Pme z)4Goyj(cr=GL#~GE$q&H)0w#aZol~Ho2S+ph6W3^Zphqo!|`s&vZhqEMVVqnX*+eE zb0+xcFJ>t{db@q!hnjvrrF;>us~KI3x2KEiioas%4t^-p&wO;vTH)t^&pP_^dR;U> z9N%*x@=J_^vm?9Ahjz&*rS*>W z&x;up%9X<}yC+WYRZQ9!cK&wp%r;hspCW&>XEk~D#9o@8BDwz}+h<0upI2Mh7(-oO z>bDj8an9Y>U&CAXj6vZJ+q#3l-$_=Smx_IPrY`iTql&QC3f@}N7BB0t`>PM{Oo)v! z`*i;0p;dZf=_2M|9tyfBYyHjp!FFf*im24^4|WAst9iXPColeZSlst!&9@gxhYn3F zT5q!b!}L2oH&!iKHo0s-@aHSVPS*iRp#eepBc{ihGvJ>soVQVi}q^AUG|(< z7`}4bHrsixCqJBh!AYQavwFw*( z@0zC{_-I#{u=H2cnzNd1$A0KBUfbyYe7$+chAh?6v!y=|geUMFWuM)&Epx#e-|WLW zecyN9?kfn`Cc4&Z*N%Df#Jv~K>PB0J}=9Zp`3`_0#s31`20bCBcKs*gXenKmn|h+lutXlaaK@HEct zF?BH-uReX&^L3xQrJy9?%Z0p#&3k`0yCN-MrQY0o;k-O z`s(8&dgnSuD;0J-DOcu)PdVYyJK^`$qR9&%Pc41N74UlRdYup@1Los4J0>h^zf>hz zCUDC*y67X@{2ym0TsxFy72{s-oh}o@+_|LepG;(<1e2vo{7~*HC;yaPY#%K z?Yp?bq&sTYtBN$39NT|3UQ=L@62F_h#lU%YNps=mpEE=y)?Kv8U(6lxR#aa+`q)xS zHvO|N4K^}vcvMmN{9Dt-?JjR8tWvGJ*}ibr){V7RuT@t}%Jvm|)~NSwqnWVDgLXfy z>D4<_Cxq^f{dg^d@6xGtuRHHv&hU|5wlSY6UQt8k(tD=MXBTAZOZr`qSX=Psj#SYV z=dGvj+V|ScOq=ZQ9#D5pXWPxfi~4WQI&z*5*tz;Br|E_J&3ghTxA*z4ckOkr>s|9k z!pNa*qvL&*L@p*R6S+n5rDxe00)2O_zWKSKL3y2=^N)lAX>0m{zj_kG`A zIytrGO^u)Y+@ky9=1bF8>|;nwS|Xfnee}5G%+2y~I`8KMJ1}hf(|YsgfeY$A^H0iK zAH873Amh^TgRxyBDMIV`-{;Az`L~={yA^C$5x)(j}-DiogmHw{K_Gz|wzuv!sz_U$1&KLjrk(lJK z6`k?=<95k)#cjvWO@I5~dcqs^f`TunE2{<6qh;H7$o@Aw6S`}aQT{XC#4 zE7j8V^2FCYr)Om=yyJQJ&h7oVGwEwzzSrHzuJN*Q<tyCjNE%n1U3}t$fnGxZ+uger1{;C|=cp>& zZ=O@v<;?lGz}os$Kci-yd+^jovn5<#Y*`CWD`m5~bMBdLae}MiW`)$pSH%-l4d0|q zGqPFwOxpB9W&dicHMWhR=~{i>i}`anL!aC=eOuQf5W=uPefG!tC5Ma_DLZeP{bbpL zC$1GjCY+ZpS{>jpmt)w`_)u}yb+!BP{~2Z+2z}_e#iDSP?%|2*Y4LSVor~6Ssl2u` z;V+-!9{wnL!`ovUqI`{IPckyrdr#0Px|>yY%27^r;&sKFO$_1v{`!*oksYfRC$_OL z*M7||ys4O&Ywg+j!bJaFl0&ofl^M)Dg5{qidAaVeEjn)QZDCx=(pz+Y$8FQt1-zY~ zQiVH08mo_ry56okr@`0m+|^@||L3*H5~+gaZka4w*8OM56LzmI$iBb)%p1ppJ6^9m zr&Q}$VyM+o_4355dD@r1H-wshTQ7B}`?}+s(k4|Y*;cQ|6;sz-yVsq@womRuUrA;GLPI6~~-_K0*b#tc8*}>8#(ww~W zxb=aXkFM)q-??GCqsi{ak$W!6Z`-m%e5tJP_Q$+t@1qo_FeWkjf1ERE$%f#q&)J$| zi*4IVLob*-X+vewtCT5p!cHX&k z-~AZdB-QL|8mkKTZcCf4_}b*Do%tHh=iBba?ymd1z*9KLUh)|8W>)5N#aK?fFg>_T9z_N`C`UMZMJ2zzT zKUeJOI(X*Kp9gyE%g$fB`(*Wuk~_O1?=ji;hrW-id76{GzAJI1)9M?BdE7EA!o0e! zvHC22!j=ydKTXfuoO)!6+&zJ;#7!Ypv##r3vlV;8_RIb8VlET8>k>^DckU2<@?^0H zr`yBD-XHHNe!uZ@|B>T{DVId6@)gasc&gO{l*4|(DDt#`!T9N<7 zKjK5HfPSXZ;+_?{zJ^QP_pk_PeBLU5Pg#nw>~3V|VI|(|#j3uqWd!%C2`#X?m!csQ z<6GONzGKmf+&P_XTN?j0Zq%C-JK@As|3!|Mt_#|+Eqsy0ztFg@RfD12B0l&%d&BmW zUC#rAEq?Ji?oLDzUpn{cc!^sLHD_PVvZ`8f>JaaSJqOBO z|4d2@kWLOev-g99!lK)MWM+LmX!!MN*sSy&?-w)tbh^LsWbRc9KaUrSZ_2N_-y;`Q z7W20>?uT7bCr7#UE(hfYGF+S~lm2d6KkbpAS?K!B&p+DN2zGR)&T9_*_|uY2VdAXw z0vA4RWAX?t`mA#N$#yA0vBq;;#`h(Bz5a1tocXmuB}-l%Rrf1V*g zi2Ku7zE6>PejU4Q?b-eoSDe)mzZlPY_Q=mFzJ}}X3STdKoAY#S^FH}GEoP@zR|@CW zo^*~7&!@|(et!#U=z8+&ZvCMGCGmW3}_cCVk zYsz(Tm+bE`<%&P|Dt@AI)-+i&%Y6NNQFqrJ-N!f8aMfWBhd1-A4g_CW(Qazq;bP_( zSW&y@s&aM102%j1%qDvJ}Zaux1B3De!ow{ZP6e?AZ0 zH+P<&<(fae`1J!Rhhd!NN@ zz4L(w3#@lk^~#@feVf_4^_-*7fw;@NZH!~eGS2edZsK0}{bv4a^F?_oL(8QJ$s3f z@X9TvU)F@!ybwRR{#r)vq?Z?&!sMRzJMeq>_w?O#+V3B+Vy(xk({q+u$Yw7%xGC^% z>>mF$_qOsi_xP3{+C8h#UP87=^ySZk6P7)A`X?r!%~o*BLATSEbLLx}(%q0HHFx*X z$O}A7kM?xSPd)A=u;=#281q|t*JJZJXJ5H_xy3c_?5DIXV!^NH7f2mk`&H$@{RWeL zH7QPNTPm#DEX*}mFK1QZE=p~>Hq~H(z|o8QpFHB>UpW1p=RVP_8XrNOC(C&=X583) z@B0r4j>7P$qTiO4nQgl+)qZZ2^|EvPa(3bFGDGuT(_0hof9>a=9-jUC?CS0LADf@( zgzT6YZZr4u$7_P2i{##O#rMWD2MaD$+WzaP_q)054^AqIFWxy@Jlf^YgVYY@IK#S^ zX?1-?)%Hq`UnGwUUEuMyT&J_XI`rvlox>-eJ+hL@oa1je@u1q5JGVK0#QrP}xjQ|> z>W$&+xw+R*Z8tZ)w<3-sn^{%aPPf;3<|(#wrF_fEI<|3bnbGoGLFU?jhUilc+P}@C z{xi&QnbGoU_Gh-cjE+;&fA;b^yk2QDgT={X$IPspe%bqZ0xnBG@xB+-T6uG$T*d9~ z#{x%Qy{tNwVbr5=ong^}Z0BZ&K;PAjmJbh3%sbq5_w&bD7rsTgUsrvW=k`VSBv)6v z&p+wIThxxqA24{$q@xrnkzhUz<&8@QV|8EWB>L+COEk8q))2dwdJ8 zZ7>$}6SCO5dZ}S?LFtt6nBzP(FI#_PwyY_byx-@}%%Tjl_;p1)-{&W{9x4B9f4RG~ zL2K=k>ew|GJQp6DJ>`h+gbhnyr~G-+XvolGbba z4fhH(e~Sr<`6A-icX#(Xt(#>Y-rIlAU)|j&T-`4+-`I{vb<$LYLXZ9_2Q?OJzMJr$ z;ei2nOKHH|bzA?nWeP9xKXmujC&@%fpD#yv;w89qqZD5}WS_1PksuJh_{8^~gxrnk zSDxh_dduJ}9w^@SNP>CB$^I>ue-_2oaY-Nk{GZ`R!W1DrA&cIBmPwZkb~Q}SE`IQL z=aH=K#sw?YCr2!ENWb1!Ls84R;h^^`0&ZN8K{&ki<^Rv>^7tNJd?%=*=ExK%;>;1HX z4eJ_S?PV+MeZ*LF+_&_`(G4r38J}*fmFe6ml~DezI=o`SjHes|{p~YmWUn*+S@49% z>gn&opw6#5@=r}H+Br=?wKi@>?;P97Gf(|}c~r0?<+987{9_hwd#u}etPU($zIRpE z1HN8g?r(D*RB*NJ*?r~23Kq|Gw|*Tc*;-Jxv(W3F=60bniKdX2n!AtRJ-D`!<+ks& zncI1o1wPbyPBeC9=Kk?ntfB0s_2m9FTTevIijQI5A;_2?RTy11d0uSiEJAu$GKSQ&C z=?C`i^EXmYDjA+NdHT$&T>SiE@trOa+0n^*6NDx6Pfot{TE1wequWcvpH3Weluu57 zyQni)Y>(f^Rc1fFy*asgRVdS+JJT$#GaX=*oxk+N%FP*Vzx@6l>AKdQAveG7ky7t$ zg~bcr3vz2Lt*gAIIr-_b&+pIAbcnne{mbaFNwi5qONHz8D#x=M1FoH{<6kV+JG(M4 z-||%Z_Oi6qatGJUesOMIR_{FrnaTdDwR0bx(fJ(yT4U;sFH%2dhD{28>i3CF=H+%4 z4fTDGb2gRl{jrn9q-M#n@9e+(r_KDhCimX_s`>XUcK=j4#{A)^BTMm_AN)lpxPsn4 zNWNtsbNrN=&l+Bb)n{gOKDtz!-dY|fd3MvXM=VTDmz@uDP5QZdedD1s(;Q9rJ2Tx( zKfU+0+S3QOzCAp5;H>=jJ5hl(>>MmsYSMR(9XK{sq4ZqdulD`lHTcVGISL-^_dfB# zXL5$3@SV_S1*I}8Rup`=-TEd#y6@rbM(;$6JJTZ>LagqES3f!ze6vIRz`KWfyn0=) zrIo@L^TY;RTu~pf>G=9`<7d{9&dnxI`9^sERc-<%8=ebnAIdA`uotx&oew&|IdDua9%^kLkze)QR%6#s7V0Y)do#})% z8Li)0&$h%eKfiMJnBSormt(&+JL>2?_TRi__d52uZimIBR^58#mw&q9fP%oE<1Ac1 znIDzyh?IHq{%_JBo~@Ht@=S0s7qGas|6p%nyoA+(tD&F5oxdcV-5U4yLwWnL&2z)Q z-aWXMYtr)77h5zg;9aD7+E&Bi`VW)7W=ZI zF6rsDjd_ocwN$ZLG;DkRJMfRf;cMF?`~%k?(AUX*^Pc6_{esie(-o(^v3px6V!h}& z>lN+tJ&X_kyneH9=C4yvX2@3Uj(B-pQiOS7~~5~+TtQFK4DI4 z!JACcBX2|wXY2~TymbE6KN6RFL%Gew_Uj*U~e ze(@j23U1h>Yu?^ASypfE*P3s)t!F;tnse9xAca3O+!UNH}By7_SlWR(+}UD=_;{Akol(6&bN)8KMNumUz~jMYfJmvcsAbRbj4lLi8FFL zbHr+Yc2822zEHF`e`Q-5!-|Dd8SZiXl4_Z`FS)ZNFQ(;s!=IGDU$jcI{Vs95~c{047ve#ZRgkMRz zC&xENKv5-jy6GR*`wq@Mjv5Xz{Zi$IIhi3=1^au-H@kK-jzB(@!Yn98C%jRXtnmqeS!J?H5U9RLk zYbe$_SaWA7M^<;}-<=9q+MZrNW|g6&_Q8*tLE*c!SM9%zY*$}zui>hXF!fTIrPuqh z(@E{F$~u*zOFI7+5Hvcy6D**lO_UW0u9b zJIiMqDTI_gOj^8b`E90WivHw%8W3^+^h5hJzTVrag_-mId^GCxe-^(c&+G2~nCRYFLihFG z3$FaqvVYFQye(n#OWtd`J(1U8N}KQ^H(>7>hUOQ?(r$WPyKnJak?+IS=yWeM(?lV$vnX6fO&*Y%PTmHuaIu39CW`AwkwLR@@ym^aY zNV}_V{>oV<+^4HFRyI$%>Zh;cvHD~pE2G3&(tSY_h z8RB~rE)=Pl`OV>wVNTB0ITv?2$;$Jd+&_V`_m6eG7g(x`z7`C5WZB-le!FhY4aH@R zPmeN)C@t}qiCtE~zKeUqcjnfs84dS%%2^iLm7Uup_BU5>%bsN#z6*9QUhwt1Xkg=_ z%Pq1yxP2Hkj2#Z#a$&W8|Jw9q(u?JX`&J2h=;|A8w%F2G`FTasyblb2)jNvzFT4Kq zMh3r{NVd7-Q`&y+`BlCGX`U3^t$wrjoIa>;dj?%v;g>QS84g!a389=KFR zdB4x$Oi0d*<>38WI4$UmSpOcDABJYq+-A0c>zCc}$nTLqX!3W*rN&FmuVw{rdvHxc z$@_luPm2l<|AjgHaXSwjP|-bW?Gip$qI17{{re#H0|{S}0|W0m{k?UJQ=9uyNx0KpYW%~;H8iGID0SJDoCX5ns9uhlYaUmf{n|M%PeyLN`{~&$ci)=7c>cjP7Z1sNWi)NlqtgLw&;B#4S-Mfq?AZJsOU>}9 zoL5gwd%*r;e&y~D;TsLMJ+!Qydvf_=?ZW+OKU5A)`IdHZ+L@IZ(jOvYlx?KNM3Tuh3_7KX@}7f%Gv)LogiZNl%pjQ zyOuOsUA||k5jDB-#av1K@|?J>pL_XU-~aTV;jm?po=$<2$bSZjEpn5VKC9W2#C3sf z**WdNP2cY+De+x;n=a1yrk6Elo7T0eKPjS~-;del-|e|~=H{vo^Vj;P2}C}gce*F% z#X*jH!CYNSRD*vdpE0$uc`}2Ev*oe$dxzrhg-uQOndfSD7`jbx+TF(1bxG&WllF5e zE-Vk`D6}3t5w3Q*BbP zV&R_uB(CxGS#Oe9%eQLn_ev|q>-V3fE?D|w&L>ILCtD9#7JigFq<``Y^2_fmdovU*vISYL_{DwqpVJ%Za!#J%_iGi7D#87K z9tw+!8MqhtSMS*hQy|0@$OH?Ir9oK$(zLd*oRdkm0kPq?`Dr38bjl$G7-%X3etyjj;SvTy|>Z}+RpNwd^E z6nR4V5@mw4pP349eaf{A?O$@Vci*8zUz@4oTMPHE`z-W=XYabxy6Zkxu(x)v+jr*9 z)D8dq4A)8uHJ0)Dl@;&rF=`4FOnhmi$2}`>_nX(VCrnRTF}eS6M3D0h>1&_n2{>vw zr(E;-7ro|y!h#2D)-$mrEwnf}@kQ$50FjIDBxA&nl%(yOGq-c85^wgYhfJ)6Rc{Ms zpZL$v)6%%Bplm+lcD++)qc}IGDKxGMHD7$Q(UwE&{^nnW(iUAJQ$nQIT1yG zjJG@OywqL9#xMHf|ht(~2r!a?KZ2KLV{W`j$dV;d9S$HI$ zqJdlJ4L0^U8Ev!cU)=m&cw~jq`tUvLJ^CNKzg*$FsPC@%`yWQa%hxWfGt2zpqkPQw zTfy7IQ}}x9E4CK>(q`t~n?1iOU(O(tQQYm$#s?eb1Bl|;qyKs80;?rREc{`VG-2A!F+*EaO=_-k~K#K=2{`GBQ-ZAG@>!aU) zAM|HgshS;UwzVvhY!T!wmr;?)V*A;9PwY;#kAh#y- z&bvew-M{V6RVV4qzgp0eaq)_w)~C=}buyaP7FLxvCvPtFH-7u;Bx}Z!+b{nVBq$c; zPYxGn+{-j!*N#BtfLOn>R6!P5_78rBg694fy0cE-=3Vsu_%+TF1Do5NhO2HW&pR#| z8oJx>#Upm<_04rYX;VJ7F@;a(<*NCTy!pz31-#c{8*kaaGze0RR>vA^Oh=Q;yf^UeFuMa|rOF2ia4 ze+K(KLM_Q#uT;%Yp0b)>=7O`tb(>dz&iH1`^1Z#!a9jS#eV)rr6k=JYuU#);c0`WF zuSNFJe};r>C0Cdt{#;gF#+3DrjM1MWBwUlVrrF}$0wA!TrJsg`tzT`(y^w( zYzp#uyN;^?6qSkfef_aq%MHhCj_qP46da_Av(d)N+G`EEZ zy!oj&$9MYz-UZIZujM3#HZQ!~q1K@3llQRg=GqDKKTno@VInZSx+pwjBKwoVKt`6p zxXOEBbGa=qyuF^Lxsh?tb_oHgw@%?-`(_?sdwcKWr1of)#jp3o^=zm}J8+-BLV{P# zdKcUMf`4gYuk)nzTc`R+B~(aFkUpnVrr$pI^9zHQPC9+&*Eg64JS}*j!ur-x{9kJ8 zC8hOZJ9lkUejfaz*lYIRowY@b4Xf&VG+g)ImcKIl)S@nq?%%(T9JzR7Ki@mE*}a#4 zKh-t(sD9|jleBGn0uM=abiI{y`*F1LROWi|^D+;g=xZ<^6_=fzyOsZKp_92;X{zD1 zV-r_dNqyFtA|!w4y@>6qvv2QY-B|rfwq9!H%Jm!8f4nQBns-X~>*OUO5|eUdxEd3) zY{bLaEiN-&(fqT!elz!llR34YijRfI-(M5E!QRR}LZ+gFt$f32>9v=cw9L12oSNdE z6|&=Wz`FLrH!~zk&7yN;W8AJh5wuV9ydCE=_w-!PU-_12mzmC;a(1tty7l$N&By$Y z`wVB%2K*ql&SVe&ev(AU*=m5w~`LoOC8Z`B(SbsKMN5AHmWw&v+V zwwAV!TZP^o5`NCUV%=(mR)tlmscmvw^q%YqxTjNii7{_$wa>y3^WDyYr#H@P_L5)w zeOh7L)k%l7cFOi_D_W+qA*ylVQSN85E5i)Tq=WAp*O{}Fu3q+^!HxBmba3>R87w`a z+&Qm%f=dbx#o_twiexca#{dA_(f9Fhp z2FC7)-uT+JfSXys?A*E;4+Pm%nN}F7^4)!N(D2l$T|epy^rtVs{_W8 zJ8Tmzm831B|0cPATCd%H+2Oc^{js7W7anfOEp$8+G)E&}+{xSDd2V(dx6^^k+2M{? z&#qc(TDE3K(DLM6HR~cunCB`#oO*ew!jt2L#ZRqCJo?bI!(iRcE>1-Lo2P!S@_c7mP4P5o$J%ir% z-Ml-#GA>OzcsSP3oGkf@rL%gQ!q;s}o%Ck))+v`>J!`wA!E{|X z`>TPwXJf6}E2VEfI|MJ@`7rZbvdWRFQ_UyuI9`{xP@4TMYoBe>q$nMoJx6N}J#Fz^ z%dl?EJ;sr$yM1;ib=9c!ihOjQVB!76?M%N2s-Jg;t>=KQ&yHl5Q&+S5R zMRHkS$v(-d8J7EO)$;iiR>-<@Y0hGczue5Yy3_M(!H)j?kIR(>Uf&Ar?>LZQ=}~6J zzTZOPnS#W*&g$b@jtrUJ@pDX8*VaDH+Otz?;+lp-#p}*lSb1IYi*Jmnyy+9qtqAk`xvA>%9K=DApvilYW zad+0c_IN01@=jmFt<^cFNftmTX2=lEk3XaYXl5IbDlDT1d@aL{mo0v9a{4p21c>LkkT_Q)Hyp`fyz53G= zj=+D9?XEK=n96#i!a7Txwe=3&r2VLzTP4AoEB_VGkp_frb{IBmc`ed+m8|=`%du6fD@tv~k51IYBSLktP*N@V= z+Ot8s8F%Fzn8x|$QiSV~Fh<3VlfUxRIm%35`8VNWUionY0=*dy#yqjUoqzskk8Q)=ZQmy={F3mx^>;?-<^K#< z|7`qz(C&Q16rCISizLHax(ovE{yZ{!j_Z}fKh$2YU;N;JtJ{Kry1i#J1Z0|S9@~9l zYQyr@%EB8KzUFcN8KjQf+nsyU)8WvCmIdP9b_jGVzPI*2gW-fdE^|3gPilI8++FnO z?1Ow|3FfOxzA0sVdUTF!;|`u>ym9Mo60Yj5KA*?5QDD&#yJp>g%cOsu$TB;9(eiju zXt*n%<#F%xPg11%t{nL1f3u^bFyHsK+oJ}XkFl~37ApKSKmRQ4?d+!0HdV(1TK$XS zd~`Z>!q;jyEo61Ey{5G2bF|GRiwR{9?epxmh^&oXXLGcb;m6`jRoTKeT=rE5yd7K~ zdKEo4;JC4;C|^2-i|;_Rv8T*czBwh?bDy2txo%&$tjhwAIYkdxRGE*K%kOas5KTDW zA2T=kj77o$SB9|7jmHILCS>?emdZHXv_81PBZKeY!N&{qvL4#AJ$t!hg+io@e~8hu z$v3CfN0cmn>YRVE-;io+ ztmvz!7^h76Q*XI7mic7WjP}?m4?eeB9P2!}Eh6#hw1<~k>Iyoz9PVvjXSn)F2=BR_ zuX&k#TQ~ERd3IfvN^5=4yW=&}T+3k!OZfKf~j*dk8R4( zaDObJa#PjJbi4LbjiZ-N&bnZ_<4%mE+k{0HS$PRM{cbUjPI}223p=ptES4y7;d$KZ zlE+;3Du?ni}SeAUI=52r70^V6>yMraWeY$MU6&`L}B9g*)lZYH&@_u3_9YM4l?mz(L7U6{CXcBtgx*F7tX*KifSf31H{J7bC2@oYBRNxBbf z65m9cG+#Y$8^f0JlTClMjHS%F`qw?Te;QtRaeh`}@-z0w6`lba-oos!S#$+v2hHw$ zC(Xh1(d7Hb<$Ow$7w@>Uh*4LS`-w%N)@}U>J_7vi?Au*E7MxLjkh)7wxF<-DUuIES zW#!pbGetWt-Eq+8_KGxnR3=qoapdQnwRs)#X=*wQyKV-5ZOS^3ZswH0>2{p^(Vsgd zudm7ekZ|8|D0}A0k6kAU#a6|~cs}q@a_Ko3kf69=Zl+HGTgTN;qW>8TkFJafkP3Y! zXtuFpVVmr$gr?J%pXJJM<^74Bxj!&^ng6L7Nnb?ntUuc6?~HCYw8R*?s{BU+>`W4lW(WPL$^bI43^s$TWpMap{&aLNT-AKsig&5 zp6Jv2YEhY?QWwpwKO9Ll73)7L{=+aaT4Cz;Lw}Q6Z<-wCQTpmDt*!p_+!iT)(^+=$ zg+*5l$}RNO_I-BM%>MGd)zKlRBzsPZ_Xhn_)<>`K3K(*5;PuyvmoEcmmRH@_cVhWE_cSL);#c36kiatheK~k>u0UWJ9bwaRjGW{|M>4w!Gs%r20XbBm3}>V zEA4mt-(!bMx}AF;Wmx$+B?zV{ow{(*+TgW`xcJP%j+KF&Uu9;-Ot>Fsp>)fG`{%U$ zNE^oV6%Upvc*%gqSg@_V+Vgyl_srHG%Qrl5rTO68t}SBX&ywpr4F0a$ z@mV1>H6ve9$~=6lnc%_TSxlm9munV9dKIo*+Sr?0_+oOqnv`qwbG9#Hd~Nv>+9~Pc zem(gYJP%$M7GHgObLzc6J9B!^YH%B8GUjek=i6Q2Gj(b1-Q#CejGTi$WXC>EcAhmW zz~pIgN{|_wu}||0xvz5?tHT|x-C^Kwmh~ zU4&6~S^H~qU+(Oa=hf~oCpfNJckS?U-V%kpE06C~3Fv!m>zQb}mf2y$>NP!6t7Cte zGfgmG)>)yhzs{*4Pae65_v~8}dTGhcmIo)qA0|&axv%&3_m`b28UjzuTaP;^ zc;~kL>pUi)*RJr-dxF4H$1mcwuX%N!ZmDDWS#(0^<|Mf}_cmS^&a-rKRD3yQ;j$%j z?=gl0uc~4=I_-y_NqN(xVSNzhHiDgDmI8@&$k1R}{_RJ9uBTaAJVo_u0xu zS{G)7-uf_I%3!hY%lbdZ%~)RTt@xSHq#a#zBxR}Wz51J{%O@Y{Nx$&sy_oaSNoTLG zVPVUA{yN~Z$z?4T^Cc$6&m}A?)Mcj{JGd~+ub1Fz?RH(~Im?&*CcE$+VSzdRayxc@ zh%Vt1Q47pqm0fi`V$JF##~q<(gV%h&ogtTL^dK;jY0p!Obq@~nM>22O{M_`2_C96a zK$mR{UnBSM#{5Wod-un4wvE+EOeM3fFSVT+QwGeGl~REnMWM_n*PyQbMP2_qyq$xvipPzA{q=_L`t+;Vo``oY2 ze9jx4El>FJWqX}B2w^TUd2_-nhb8Wz_&r9EMz@8&^UbZbX0b%q)D>pPhZH@U;lwm+ zNnfvhgrbJVkJh|z&skYoLQFn(aUKZEI(x?>1Lu`>tLHvu)zW9TP@R2NujjXvyPK!`-IeQwrEaaB$+NWG=6yeB z=ZSR@O;Qh!b67gQKbEBVO!v`;4~J9QSte#z8nsV;+nJTpV%~8hxm1?3H)BGMoZtS4 zw+Hu~b9;Jol30>;%G+nlnmM9&+FkCS^Qvgo&%!9Nh* zBses#atuC^a3lB4;;PPh<(`vnJWX;i&VSyssAKzaR;kxde-y1d`=8<0?}MwHkDjzv ze=e})o0*kqL8`Ws@FXjP?Wx)#tH1v~_(3$xSJEO>!D3_Oi3PFZB{nmdJ901X%ro6~ zaKkcd$1m^g8n;a^PhK;*e4AWC>FxCEY)Z47tgje0FKPOHyYQ@%9COruH@(@PCN~@} z5-$GtwRqRtg<1RMrLSdhO*ojkKK^y1R{00vmCcSNCYR*WCN$*+Sgmd=j^KJ>dgRup z;tBIECGQT7(!amBpQ%GVf=$pX$ENUpa!Sn=;a^84I%Hf`_dR>?MaQaH^Lth@a@gga zociQmb#BFs2}@l+a&G4;VESY3yhbvxF{^Fo{A*k@xL)Y2n9(S9ti9U&_wfY9yiK3C z3*0>sct_@6!Hij9)4u$h<86@iraFdAJ0!4H=;Po^~Xi`{b$IV?rOT zY9+2nF_rC)O=@ME$a%}8sMVEI{P-M&z7+~-2dA*VK3mWE`)Cj2w`c>guphkpgX%Wt ztlZ6g-MY;vJJA329jV<<)xS@zoapt}+4`&{d;HOTh8AD%^6yPpeZtc1qVA*H7Zf%< z?&7^8aN|hSjPS0Pf1X=r*)J6SarjQd?ta;X2d9!*rhA=QC|%Ca9$~n$sp@S?(z5G{ zTJw`9G)Epzmw8{gP&x44io{*!E~dSoSzm4Z9_wBn&T{xy2g7{HCaW8FOEM2uN=JXO zF#9msM|1PBRS#tP+oWE0hPp`!U5MTOY!j>7`W+Toa3}XBt9>XA#la}KzX?&&isq);fm|m+&!@GvF38mq_vYu4Zci9o)LCPh)DVIqoS8&Cw#~f^)+9`F7Yt`80x9{KCnOqM;j zYPsykj}uulYW!jri`}xYIsPhDL8v<*FRH($+U&X5#CL=VKvRCW=Vhyz zEY1yLMfXhKxn}8|-1u-h^ULl>6ozR*C6!@{6)i6oFaNn|x3nm(xa9;a$fIDGv@oV?@j2u^*qpJR< zhAwhmyyJ}AMJJ&>KG&{a68&Xy(6mzDo~?*E;CkiNw+E+ox$V^c+~~xi{-0r=h0AO! zYm0RvyBL-;yga+%M0_|`Hp8d({|t9rJsaMaKRIMNK|Q@@Qt_O*MGAY=GAlD8oS*Hu zx%s|ud@NgrQ})64Q;!5yXRf|(ak-YI%#O`Z?o_PJ%$1#6FG;_9bI|Hacx*-7=eCnV zyZ6p}&S%-j?OWV2t1El$A7_SzoDPv+(vJmYuA8&1Y=%qDk868xpUV!G68m>}xooI^ zl-b-zNit^k&6ScDa+ze4u1+@z+Gn}M{n(aUIZs~P;wnFVKElE!;LE2+MQe(Btq-2# zHDQ>qS$!}hj`uqcN0mnG`{nwsTRw-^{wr)t-Prj1n(iMqzTm7K^I4dCzaHZakg%$) zmAD%aZWmHM+mpGdd4B3a^KjGcJT?daeQ~v7O ztGa);(Nx}(8i%KUzNaRo_!?9+c$FC)vCp5pD za3X*1!6RqqFgBX)ZQp-RKup}@(4WLv@m(P`v3%v5U-5P7iAAzcW`QPe7&o zh2)N1Z`}S#-tP77<$HdQ!6!&J`1IWwt2Iim-EmpeKJmx8fPGDmcZOn79&VCMsax6TdGo|A<2~(CrMDx^Z4PgW&ExZo6x(2C z)?Tj3_;CG|_R51##1HD3CanI+tjGB@wtfynYL3F{xI&LR*S=RB__WQ{)T-{^r%PLf z6ZikT7VE@N;5=F1%Y9YFgTrPU0`F}$rR!*p}SD( z(N)jGk+QM}X6x5W?sqq>%X_XYwa_`_Fe|z_|%R~%G{_s{~hbq3kSLR|DG~;x6ivb_Yqsv zlO>ajg$))woAJ&k)AUJ6e|+C3J4Ie5DgQMyRB-+8j(f;3ve$fXyXo%o z;={f(%+nUC^DVckIpwqB&7?n%n4{X*#M`V@pK`gZyI^x#zcl4qf24En)g^x(X*4=D zvS0kJ;h0)7>!E=(pE=LE{|q?{z8wiC&T-gl=ppZWHur_*jMy86||*!NO^GUxf* z4HKID8n>~3d%Cul`}o{&rCP5wqE!;D?&Wjk)Kmhu*(iNycgqT}ew=N&^tsUL{|qI2 z9tTG;Uf#LKOzLgj+PxCBD>isaIx0j2SbdiG62xNOU(0YU)ZF0N)Xp&T13R|gd;BVy zY2EF;H9MxTZ&isXUaq*pwesm9-qU+0_dOG7T9|n6ZZMcjtNVq68 z)7RqCi!A?Te|AJCUSJV>x68nR)iHqgLsDk@s@|~3ZpT%coR_Q*bQ(wT$mo>znpb=` zeb>z1ymsbZpJl6d?U707b_uzEyV@w@$w3ae@26We%vqSy8E5ARpF8r|?u z_#o`on?n(0jkhm7TJ?SXyS;Z(R^OO^?9;~V`sv=kj$XaKxBSQRu3M_E*!9Qi zNT^pyscdrDhAGE69d}G2D9jkr3#f{k;%F7NMNBk<}#qk_dX zwO%QP2Qy!UH_I{C%@AYZT=(aI_rkq1Z4YQVyboq=j@`cX`J7d?Qfz!iISZS73-0vo zty$h_d2QpurR@(7%-pERpQLmusX6e#o_s%%*^7UaNY36<#;1_1e0E~vm+Z}p8CQ6| zpJ&9Hq7h9J(u-f^3 zaF%h;%5O@XvV3xiPVF4sT-KQTGh1w3rh3hYV>Gy{QqZ}f;DLnZx^-Uaj-RT(-}8%B ziDFUjQ%xz6Qaj&aai-UOv6ub1GmD~@FwA#*vSWsblGcM|Zk@PwSVpns#S^KV zs^dHK7;cxbnO(Xl*-sbmNiFq;^(G&OdqBkk(ju9f7OAD zyEebBO%l9oee>@8JvqDCp7fo4yyljU>*Z@7zln#31bAONB zW60PWb-TRRK&t8Pq~~%S#n&0TBXVyByIkCBar&L(mtDLmA%O=OR@KTRNcFKby=h;6 zF5+g(5B2=Rl}cNlef3?~VW@dJm$NRtg0Jh3NnYZby@$esbdF5-410ZX!bNLspOUP? z2g~$bzlgKQYN*Y-T>I#W>8fs_%P;)S&5(V1z?0*`^<|8r%V$i#_qF;&Lujf&>1QJ* zbE!*93SAamGivy(CCb;y{FvpEclDZgf0jk*id44$*!x(dQFy`b>t8=kF=#TPGpEW9trlwmP5tAbg|%(JtSm%)YSmP3CE-~2t{*OsnY#~UYhW%m^M z`G#H&U56g@`5)d?Rg=1+h&|W3_=c9yzB!V{XZZS_&N@<|b>sc)Yog&Siih{`dgQft zy!<1q*cI+@^6}@^V@v$=0!)-0^R4ucIdU>dZ+@{~VycMdi|3j=)|R`jFt0uRZi5P! z?8|k(pGzFSYp)lx?({mHQXVdU145xvzWq`iy2Ip&fRh0x8z-5 z_4!+pV#fFSu15<@ggU;*gy%8bvN^whO#-(=eV$FS_vs%$4t+f0-Xx>)YyzKV#FN|0 zUvr0V5T2V@9=NOgjX~U_cRMV3-|M$rSblrU`MKrC!Z=Uu_@2}6C7}4u-a>TuuJYtr z%a?61)5`dIDmjjC`V2cBSA_+&f*-ymSxbls&i(N6$V$mnho6SGf*1;x|2@d=v74#< z@v%)4TEp{}yfxgkP(;q|`;|lKngZd=&IEryFzwK^?QAW|K6_78TAymz!_|@P{dJ0s z);poX55FEMiY--|s@9%!C_kVqvhZEooxG>9jhr#jFE%c@5gT8q{gHi_$b5-;VX?mM zq7p({Q{PUWvsKGsZ=p7#DpzwF_XJMMF5CX0Gj zJU%^PkAwQ1gReQiuue+2P`ajK?heIZ)z|J;N7c4n&Axc`#3#QgGooCyL&Q&Z@$_|? zEf?0#TVXJX`587xY&GXK}e;FkB^z->>~-CKO_jR8x< z%m;TaOL4nket_xG{Sk!pL9Cp4pvHE8FT8K>7x z`=xlgx{yUTXyW!gg+lxXzn-%B{Gk13^4iA|&W#;1Elw}CrG6<=U14L+wW-n1`wWNK z3hV5~H66zrE0*b8(o30m&a~-t=cl-j41rF~4YQ0Miu6ysQk8CBS$J@|g-n!@Y;e}k zX)74z>tuwwrkHGRtu;JSEPf;EPmJ~A+_YyKynLA!*}VKL;(hQ5m;1~~pO?+uVX-yT zdx`n}HEAsl&5vFA3|kj0d;PTb=%r0pWA|n5F=Kw^d2HXA8jBZ?{C?=YaM_{RUpMve z%(goWn>5~Zyw8dHwNS=u#~l%kvKXIG52sn?pBuWCSnIE2aZI{%DE41nfygZBU5|w) zEC~tVD`c9&)q7P{R{a=X?BY)){5)%JZ^}lBm zwso0(d(BvBs>8K@>H*~rhTPic5uQA`dpqB$}9l_7W6*t=P>WNl zmpoj3zW%{k9|7yv{A}0kUw6bv&6MBz`ixG<**P}ZHw#%AOty0VyHg!vk}%Cp>fQZ0 z8?V?bIDX;Hhr_O}drj9)7pW3hS@L*qN8PO>9xP06vdlmB1cc2Ld}miVec{Vk&wF9_ zJ?67?er?!zlYMUZ!B%ND_1SUJ+tN0**RcF&Sf_EQv3h>7c$jncrcXP>w;bGfY=Yse zO&j_1Pb7YxB(=)=fpn9^qrE>~9_43P?>i@-LFK~B3;%q~8W_*#+gor~-c7H1^e1f( z!|k$ai`R>}kEn<_xt_Fn$+KH<>yd8P4FR_gq-w2uuxW0><5RpW7JV#xxAm^yzlW>q zwLANf_ncx(0?HqYbXP^6{q-lou;Yz~e$A7Bz{7W>hH~I>M;rLbaw{7uT5U?V+Y5@ z%dv{T&V;SMh|&P>zQy@wilSMfd5 zT_vqj!<00!oz>*><-69W4{h0Y>#XgQzz`7=yGN@ZPe1&*fpx*FRmS0LF&9cyHrGoZ ze0KeXY|ot{@fUS=8Mh z_L%AA(swgj6v}!}sTD1`Huv!JGt*X@EXz@SeDVF3gDWn*`*^sqa%0KXlLo#Ta^KG! zKi1Gy9TDTU>#Rfm^6L^4Y(k<}Z*S%ZjS!9FyRH+J5peJ88o!AqzK`c*-q>2k<9K-T zCE4{cu{;WHO$CYD<8E&(d|A}Iblo1F%e=d9ty}qZqKN)Q&2!#so8G>uK3e6X@gZ)R zLD(;E2VdFQ2fsB}zb@T7L-b5Mx3j96IrDXiK&B}dcvsJP<5045OPxuQ>AqlRyBHqv zguH)iPLwftUovzzeRu11+zpmjyYpB)8qUpo-XHOU>-ge#kF=(WZ=Bd%?a5lfa_Y+_ z&Clo4WWu^@nJmp#{fX%~^-sEC%kJaq^EXsXd^{=HckA)=dXtP8QRat9l{M$g9p(MO2vuWWgA&1%O`yQ!05fGT}o4B&VUp-&x zXG7M`Gxw!s7@ah~7|yOTcs$E7uQxigzzr6ijZU%SgE`6=#e>-GmoST zKYDmbyCX|z_aUPl6C!q9JzRaOps4fLfm@FjZBFZM?Pv2lyQS*UyD!4CqMtu!a&C-% z$NQ^jF7M)wJYJQeizxNf*>*?mRzNX}|)BC3R_nuoj(`!u9 zrY=no4H7vt`^4>q|0eGV$!xQU&32R1ty=hf>X&kt=#PD%{pJ(ch1Lmdi+KDhC8AIH zL_Ck-YE7&B`)l<2eu|lkN9CC8;!-dDF2UZociXR{+)M90_j{By$rgYu5ITe+LzyX)$)O!H()#E8D6)1*d^j4y`Go5?l9xDK5y?^Ry%Zk z+m1?nx-1j9X#17#@9!*Q43Mz%QCfFxNBXkb2NPac9%1^X@^`RwJ+FGbOrTY2h3sd zuf`eOn61V1*3d;sFyJ`5S(06Vp(xkc<%`1Or$2wJ&~QQImgrHrho_F)C9Pf>Z4!OW z>4KB^++Ej?X)Zi`=#tBiBDam+idzpZ&b@kKRpU)3eW~JSItD@-Jm2e2&oAS0le#Tb zI!!dn;&D6A?MU8_fnB={a()(eTnG+-ETk;_dlMH+z%=tb*2SrxVtW){?DS^4BX&SB z?LE(($^4HV`6mdeC$BGBXp-Zu-)64UoOx2}?@qx?ZRUM@j8iqL{-qq9xy1KL`DTmr ziHfNlkEZp8zdarnvfp9<>PdE|{=AuT_RtK0tnMZO?fp)EdRiaL1Y z&v<>oJ7=c-&67V_%#;k35G@fneX2=$d+3{E+FsM*82+x~xOO|Xn!mYc!#v$<3y;YM z)-^ovsc4yW(N{|1iJG#Q0^df-*|lv4jtITH`apj&m)YVimzPwYc*VH9`}jj6ts5M+ zIadq6RI-?FOTD;S{IX$|^4;xk4u&q1&YJht$k}oI+*4&oncuMdedv0}>_5Z1IV>C* z{`<2VCS-t;jy`)V zK=W`yOnB}o+Zz`jy{k#?J#n%+ChG;u-;MrjxGD~DtUQt;%NKX@U#e; z@z{pf+n%*tG4fDVUVrxD8lJO~u{J$V4mo)^P1_O9SzO+kq3^!y?Cr~q`}alM%3%~s zh`K4@bLY_BT#aS7A`&huicAjAc*y(s%r}P3(Tf`$u2>|s*-I|^$SVdetq5L-+9EAJ3E(^qy{bVEn9bRf?c79gA0@Cld~uIdOMa| zlrB5_!SWhs>8sB4f|GGU8E>9Em)UsG@%Imd#R1-)ZOojn7G)N!Ry@gZ?Byhldy^BY zY!39^$dxWU^p*3y>A@>UZga>5or~!<72o&kz-omUX+DSc>U(Ak+RKV&&sd=wtM@FJ z%?D3!-6nRb$fZQ+ z)#N)98vkwNc3GJB^7Ohw1u_0!$9PhvUVbYE*mT`JpCzRR+c6uRT~3(K6jartx8M8Bg>0XN&C zE9dDiD>me;;`3c!v9_AyXH8{`RE~ahjIfA5@9vxXVlDb~gBh;hNKxM-s#Ctd z^>=+qt{HAE*6*1Xm6pC|)6EsRuTsP--XfOce@|1uk<)Dt&sMI=FEi9CZqNGudxqkU z8@I2=%UA47JC)(SB=JS|GYO*bD{mq>ORMmS4Vwfv~GU5uXit|i*Hr@x!>V3!e>jq_xF_iw5#05 zBfa*;+FS1|zAf6So6qz#m_vW+&mC)cq^eCDuVyLB+~#%s9lwo?6T~+&W z)4<8B-ZV{US#QMU#^2}Tf^^iY-z3*gEl`r-GtPEca!z>C!w`qnR$&et)MB^+zwVds56_w&i@{ zrXQk=Q%NQCiyT3ivH6ct}?ZZlLX})#t4mA}u7A|(p&u{-I z&`*kbIM%AJbg83VTeFRsI==m5d$!>WyHJ>HYKG??(S`Q|4m;iJ-C-iWD3wv8Li^0KGsV+% zS6K&kwH%weY2N0^swWL(cl-GWyy^LE?0A50R_B~|pGx(wStdMS{P_M3YbFx|XT>2K z4k1yA8;UHV%O{_?k-fN4BD>s7@R#1si<8Yc*7kfzRaMcDQ9tHvVSbA*t9!@&;unYC zFsA)yxIICyHTIJA(V&?XZQS0`GOS7IRVH&E9NY3bIx8-tdLnz|ujipyT672bP{MTdgH_M}g)7xwe*Pe>At&dRH9iIR0s@2iJe?Qu; zJ+;0ybHO`_Bi0?OX8UFwxEK^EoX3+Fu!^;l{m4;H!P$}9g|s=BmsHwI3W$bnn^XS1 zD3i%6O7r}+kG|V{#O{WCC|Y6t_R4OH-^Ydg81KZ3XTB@^5%qqvj$-+`ZT}hWFlV># zFDzluI(Yv};x9A#zp;;}SDRNvx9saJxIU9HcfXqDgF}<2o8<|-PbjW6-s^7hi`mg+ z>x`1GyXO2zDs#5gJ-$13)?=f}a;2`a$R~$gZr(NjFwafW=vndAog0M$WzB<7tX#dTb=phko7iO#qYC) z3*IpwDy=@66ue1Aa)E<+n}E@|lgqy7*$0??+VPZk)yK3y$23;wFt4}UF!7kl?RS2@ z7j;%o_TpXFJLhv(==IMXD;AwA+%~l=X3e*6cP8CvPVoC&E!HqkZ*GDUv)9>nj(eN- ztVoKMj);glw99AvErFTNtq+TS2r{Vpb!|#8S$*b9`E;jq*E2uvahK{8o>riyIq^Vw z`-#AQjma-wtFt%BEbPeFOg|T)yhrY#uS)OJV_Wj>-pyWT=){`++w*gWK$%IPnb%{N zDCZr`9|{{8UT^xY_U8Z#gROA*%jc3A?29k=h%E@Ze{sGrqw>;n-5Ez#5oF=P? z3g6-6TA1)Iv8U26!O+oOm;d|0r?!WQ>2~B9u7kJzPQKGfJiI*QC~wO`x$Z4T zkE#dU5SOoD-x9(q?Q+Dqz6rJ1GKEqF)5W6Jsj5F99`k2YCiYRbUv7hRt#qL-cLJ+PUlD^#-s>6nmy0hg ziJ4>06WX<4#ihK6g{xKWxKzzcJ7B`@&^9HBpU3=-w&;{|YEugwCWIcC%YRSd;%P>a zYULj8b$4f3>6vMJmQ5&8ocbWX1P}A@3%a!9O~`;j_vTD#fBTVZe3zyXUEx=?=Z>|5 z=FJ1&`FM=YBRv$RT^DoZHN9zR$oF(ZVeHEzX)#mV&NaQ%f2}H}dSSwUhVN1yp?`uy ztFG}b>sVUF5Ugf?v^MlVL(xr>eHyDn4muv&{>d!=b;XNS!TZd4qOSEC6z{96*sW)J zN_kUTNEK(j)%6+QH~Xn=YTUN=U3F)9RHg9KW7|EYqVvvFt=akDd!P9+h3t6E25II^ zOZI;`tmX9~a8A;Lm&~jdY?j{yUtX86o$J?sj_qAa$G!Q6+D{+Hl^67g>|oeq&e;&R zr6N&xLqNpeS%$?fQ*>Qs=eZWB6=v_Nz9;3Nw0`}%Vs{R?S0C4XSfD;>;o`DAk2OQTiVm+mYRb-eUJ$H!Z(Dc`Si{h9{9i~En;C0*dYy}hW; z{jq?W?d@5!p36AI#CBL7mQY&c)$x7ZrisT@15dWw9aIT#p1<~iZsymJ$*hqqfy--m zifqrAHaGWp^j3D6rFvo>0lp0X89aLqUp>{NzGD%ybTHe21-Fy-$7C}u@zekHH|2s* zqi{}8|H3EJj$7SY7|R#d$iJbFf7?;{=JwdV57(N!%lSFuL~@&q{kj;p!+JdzzdU~J zG^I7#XIhJWbVult6`2(~&K^6^XvLd4A*5~YgVz^cPJHmIKtnib+w!!Sa^vkqY%EQ8 zncn<9x;4=C-G7Gra|B(Vxc5r^D|%jC$ia8|m(jOH`xbxtw|SNOt)udDvKKhYG6r)*C@7H1wi@6^5Ty(~T3Jx@5VS;M?4YIW_w#ZPM=n@R0* zoif)lV75%bcGj@W%V$saW@lBe^||HIZ*@!~WYL7V+ZUhFFfm;heYi1WEsO51$HHd! z116Zg7U0;jX~Jg}w?8$u2J8h1qR(^dHU_vmt^K`Y_ZQ#p^?RoIw%*ZST32|DMdRnS zjqXpMY*;SJ1a zrgQY;loMNfoL;_?IJ$#PV80%t%TqaT!36JBO~KEnS0C+~C1$I`zx6$ zcRIkDUo5p$Q}fk@gNd71r~WK_CDi5^b@;JVqq*w7bRXg4Dmyxy81J`!e$E*fG@~xc z=!HJNSkdB1zZM4if2eFKInkSPp|$6Lg^QNdq(|Q_l`dPcgK@?EhQ~YqBsuUpskQql zHm%z-EwXtlXHL1wp;z&bfAII*SjU}hFJu=09w?V{RKjr=XJr;sn6jxNrh08PO z3(8wAFfY)aqP)g(LDPawNs%n9X2EBxCn!fXSEh9~9FC0L(r&E7Aj%k8QvZ*fN(ZfyW@xBE5d!#ubN(uShI@ zXS-yI&DTwKAO5<1-1zuYVxn5RaNUEwRqX$s-4`?|+_?Xo;iWfzR)raQ>~}H`%)k7x zN5snf^0bo+#on*&W4Q0TU~ZX2LRM$Qg$FTGS*EWr|Sb5m>l)Ju3Pq}n% zYg3h1-Iar09PHA+JEz=o4?n?PbxLc>?@0a4Od?yXq6=Q!cJSaAUe2%J=Qn>bD|1@P&-Lpse^G$$Bk<=cNl4 zX&<|PY0_nu4^#biPkyf(Vp{P!_4ko1S%u(@Cv=*;_x(IqSSsr|>yNf+QpfJ=@u#PI zO?^?wD8%>icr%mwaW&6u&)-KPR=dPEPLO7E)PC8>v1o4S)5kpyvR(48=UQ05nKq~I z-G)nQv)^m|U1(~nW*umy*|gcLsbkUytILyFIYM{u{g(3R(aPwUZBKTU=r4NbAGayf zWamY`=f^x|)Oq|VwK>6fL5uyveu){03$L3L2qrScI%`e%qB%Ki%U;*HH8n{lhh7M8 zI9nRq5&7t;ZRh)nWRA1JD}(D&IiqU*|1$`SaWOkOiC5h-IK0(^X~&+=lU&6@O13*Z zat-l%=W1zn- z$W9-DM4vZ*PV>vOvIgj?&XV8LxpuC0?4&u)B5Ok!6JBVxPGoa(lFn(DXxPyEQ*Ft2 zi&>2_zN&{(u55m}b9UxzhB?yv)Fe2*$Z~yp5a<>9aB=CqjfOrp>h(fDa{^Se~i$shbxw|+LR{?Oa`S2lV3#}0*?t5-gg z(v0z&c(}0Q1<%vM?JpRtKOD?D@IIIATHe`%;Z9jI;~KWe=su1xX1JwZ$IP(jq9Sv3 z^fd>abuFw#Hx4N06%}3#U0xm`J8Q9pd7+7LMbqQ`mF;GS8pvjp%tl&%8z7Q}KGu=I1=Vg7)+DctjqqdTUpE(6`FodT#BL zbeVp)IlbX2Z`IBpZWG{blKFJkf_fDZdPu1=<7tB|%xY%<_ren#WwN)Wg z=d2GFjx61j{WSl@F)tyFON(E=?=yM0hqqpH&R@y1wndMoZ(Deu?=qjFmMr_)Bhl}6 zedTXc+s&JQan2kOQT{2W1xwqVB`SjNR5*|&CulepM2O%J{~S3mic@nj{jr>2Wd6hgN=^$lihSUe?v zzd^IF-<(93I61Q~XBe~@7?mr%Z%^3g${5+uo78>yW_Kl{4EOA|8PjyvKB|w96?`I_ zT{FXz&*ScdgOB8$WFwX{IZ{ok`!<_d*ZC4G>c6z=!9(eM0(hl!x-d~a#-lu{pEVi02{5LoC z8J~C5=i1kt86ur#+5YQ3)p=PxpSE+4f=&*Pg3ZSXsmdo-PkvQCVed`vlJB$Du@v+v z6?N_BJ-{e?QFpoEp%co?uKsG$tp+zT*|q;j0*mnO^&wC^0O))-*sGS*YtT17ce(=hsy=Hr5@9#|t&o!U%^ZIMl&daLEcX&n%Zc(<83e95AzTOOb37GZDQ<5L#YqNjKEU5wkwlcEgs zf*-HpTcGG_UARQ2jj2h4@z!IrC47f8zCY}B)OHV!Id^}KnwQMy^P)eFM!jczV!n9~ zV}p~TbW^d1$d&l)r3ZO#he-*4JbFTM->htz<|ehq(57`eA1-J0xxjdBJ#%lb!v_=L z-v#N8>0ZBfa%pv+ob2b@z`4BP_ny=;i-%#vB((7*7w7t5NZdu1fE%Ho z#+w`b3ZsQviZTv1hrQY2`T6@42UF#IoBE@t(za~4zI$HU>D-G~IC~wJ%@-*C@-Sg~ z+~&E7ZI=qG4m%t0O|3{)IGglx@6L2zS6Rh8&OFDhcXI0VM1Aub`MB!}y6$E-Jnla{ zi6L|9$M^Sg;?DWU@E_xFRFCRPWLhO<@BZP@iWg_U_!Z9L*y^{|>Qrpo;Z53ql2|r> zl3zJ%pUJivdb3_0(|UZT?0fg}yDbxPx9+G;YG%2vx9gaKfV#$vGa*s@l8*V_5lL50 zp6hug8&}HCIpxQZDW)l9lm0WPta5eNO(Pu5ZAm=~V@Sm1Vdp5Rf@S6UhBej)}ToF}%_Np5A_9eR1m zj0P8ll*;OzyJOx4%stN|=(=l5oj9M4=edq-nOAYHckjPBG{K`Uc9#!RhJlY#aA>JV z)`J(eXXhkz_$;{)H+xE*)3(p^k4J`BY6T^xEjD{}RnO_3pED!FhwL>`!I`ysD!%T? z`89VIa$S_SDx9)b@$1foN@sI57AeVP^z+XKbyDSg=s# zb+}k)NyPVe3gx`B%FmwQJQ4Cr{xe6jgZ6@(X+CChECxzOTjHP3QLE6A?X9Y@Ivkv} zdXMUo4=b1PFz)zJ@w_MYqo<$n%I3#GOD|q4^xhPZYAyQa^sd!Qy?IrPT4u<}&NlmP zTrh=qw{+E^GG2A9$wG%L)_7$&pUR%;Z)j~&#k`Xzs9Z#RXYR3*EII4KK;Z=Fv9azjKzm7+RL-gNx$kS%ULciajWcd{K-~z3lwCdS z7mmr^6?QODQ+6$CWw;gc0$VDoOQ$6i<>^^@oFi`$4(a!I~y=7cv7|7gTf1y=6$ajx5>7Aw~#*l zb7p{5j%*DZw~)1o{G!Ch?^VGy&$gKqSnY81zclfX(kuI(mIGge6+hITWBIotY-PvM z>wG`oT=Ox!D)Hrw(Y^rY=6TkW_qblWt+(#(@7}ziS&yss-rH!r)ui03xary=ueryC zEqVfd=N@3=x*)*Ap7rF(HPwglZ%=AE?7EQIdNAu!M2SR8YiqMd*W*d~N$#_+w<+D8 zyG`_9o#gVqxnF`S=B#+<=D&T<-A}A5R!&;iK0)K-zIRz?)imP_Lo^t9{gWFT?kLW! zNz81LUpJlOhOew_P4%@*<+E!S$yz_JxO=uiAk(_&aWZ2`M)v#yb^VPC&+zcFr@ZU1 zVfvHIccSyMXm@zRqV+uqRhp{`mrJnBvo-TH=+NC2yXIoNjF&)joV}V^koV8ZH61tB zd!>rz`#$kV3{pIxdpN)^aEa`z($3clAI%86`i^(6#M_%I!fjT4pR)1Aa^IWjo6YoN zmot3%^I)oi%C`A)%sj+B7x*4lD)L@0x$7w}i{4^>o|1sw3-3+VP+7Gm^vm8yryUjU z^%vgLitBoMVB0-&dx?@NzV+YRSmOKJ&uEu*9(mmPwNb-a;**6qPcV;hd17DCtCl-9 zvtEm{U61MAd7!B#!_@QTyp-oVkE@;wyJBH_xZ=H-=6>bBe^R^@H@=c|ovLwo`|;Pz zRbk3ED)(E62V5@`Q~7ar!BN!*NsEM+|6U`0Uj3r|Wiy_quJ2hFinWDQ=A1OIGF^K2 zu};Rq^yf0cKVOTA+&f?R+_O&S2(SG9LZ&OT`Qwt;CImYM$=Fo)N}bYsx5Hi{ouTnY z$@b^6$$3|&N!d%PtrW^rUza!S?X*P}9E|U`TRFdlcvz0zSSR}Ya(6?b zZf*0&g3!(9#LD_>4P$+Bxu)M?OE!}YJhsARk;$9~7t`x6>S^3y)bT#@`+yM3g^7kq zr^_N|2b3O?daw0z{ef$f%BQe02R!(m*lO~Uh483Fr_C4MuY6W~^v~u=^Sn3uzdZQkD)VA#&wHzz9LqBQGw^INQd#z}pCx>A^zVX) zleTX9A#eKTV33Bi&EE93rSX}cn*?R~Hg%fmGrRGcx}WxWz+JtLAcgdn?5(}gsTq-}ez4nwtOA43Nrw366Ci9odxG}dM+k0Ym&Gxj9 z$|-?ai`>~mGWpjZ+%t9MhLUdO_5Ou7ZML?bR!sCyH`&(p7P&%Ti~r z&vuvd^Vn4`uA28;+{Se2JBwqw2Jcdr-KA>5m<3SW=dVyEO@J6Y<(n&@5go}DOjTK4J6L(`~fkLDhDdvf>U^95eBbq^#mvA-UgZO-~|_H%jHBZcGZPk(Mmo)XCZ;ZeHv z#_Pw#+TE}Iy5}z4X6?DoCD0y^gt|j07R9V}J}8})1-?0J)X`BM`A!7p(<-5#6lw~0Dz-nK36XU>dO7M~W`*3_n5wOxAiVsX@O2dzrMSA|_K`Q7vRpB&=l zT4HAPyxvCgR%lb>U7v_;s}=K)e{C{iG&yaz!6iTX<9d06Z){E56pyp-5S_s+{_<4G zkIX;W>>W!=56#V+q1L&5LD0vi$B)`4e3>!vME9jX@$)uza&=AUn?L3Bg^#|u9KoCK z2wmvD`LTNe%Od~PceE5714@tm+gI!1YV1aT~_A!g(DxWP=`E+0I+VP;jtL&ll zL|gNto7<(mPuiwFw+wRYV>x!WtMSdVe5s_Rd&D`i-J9R}PMP{=2jB7?_D@Q_BxxQx z>Zi}QuVC-GhnsVD81Be&3;xe=?@o*M5`&JGr`!*x%<($wwPUyP%;OJ^Iw&_Us%%zfnRc?Ms;*Qn^J0~ zo5?KW7VBdjIUoKWJ<6EDdF7|whCrLe7x&)r4rz(&RM`<=!(P*WPQ&$G=C$|tA_^T= zI?71AUd1I+c;$%!zl4r;p0!%hm$S!Bu!rQ!*Q0W;Q3Dl_MS^5BEL^@ZM$1BL(@6j(yoq2sk!pE$@jt+ zG4H0$2|4h79qWsutrzz^b$F4R`7a{j4vX{d!j47qZKZO*6NBC@)%;Sp(f6>vL0C_q zXHD@R1Kvrqo!_>^&p9EGA@Dc%Z?@2}wU^v?wORE|RF8ghk8i5Yp7X~dI<+^x`8$V8 z%4kCJhA02>0^hyb%(V7;<7x9#TqRc32YfHF-L^c-&@VN8<+BYZMDIIT<_lctc%go8 zPFX|ja`rO^b}31)%-=GjPQBIhN5NG?kEPH0&d*6I$?^Jaed_EZTfM^9GUrxBxb8nZ z^=hTla@&&05^SrY7_!PbLaP?XFPmww7h@Le~4eo`p?6#-~Sm5CN8qut-r?g=Aual57fmJ27Tjd`m|)mq1_jl zc3rT4CdW~-FlD!o%)Ns$|702`-4x^1{+t>)%ayH+Z>7zVUXx6QxH1WW+gWq37poWC zxg@i}=g{6|mrlu4vUMKb_tz)YHc0i)@~VPaHU;L_?%&I2+%252Yk$Ri#^VVx2M!bl zI!>(O)qcnOCeS+U-9xL(o3r$VMb!kRMcrN_(Z{R9{kBj@We2~D&)WoU=gszMjF(>A zihJ>hqjxNU_oAtFadhDrHqSplOe#>|fm;JTtMnlZKc}1I~ zSIqaHxA#zE_cE6{=KXVgr4|*h5v=szknKESw@uWZuRpFCuVHwx_IIJ)%EOZdzLd-i zxWt_G>TlKKH^NKuB8*J-9j@QA@_h8QAJ-*vUAd(0S6|ySQAuo3>7DNu*__MfJiMXL z$G7}U-DUgk`s1Q!6Ed=8ShG+28D8ytKY35b^jA+pTE#7<-YV~|CfjMQ@8vewp+-%;o!=JC5H#WIv3;)i^Oj?STF-G%NJ?w6I zVHo7V6&~>NZDB~1)YfNVwF#BWv-p)iznqrTbRnm@FF4y}-SpRTj0dE4M7glOENsYT zkLhD^Yde(jYUY8qXHyE8zkI!U(nz6k!Rsfpk3BoMqx8nX6ISZ49&Zyae^R>s$K$XL z1*JR8Qkt>5<(L^n=F6S+(c-*1e z=N_w;aM|Dg43+{nGUaNLT)XoXZB7d<(%vhh;G?zp#3TU&h4Bf{Aee0uZf8RXW zZcpVW9-emCacaqn!!QNZ*q@n-SM9T&bC=0rFi_U*Ok_J4AeY5N+pk7o1BrL-#5 z!m5`hF)?ncou94ncB{>=H!+O&UnqZg_=T8C)d)3 zn_p*dntjQ5y55tl`8QIpHE7%_o#Nn@o3JlsnXTDB`SkOaHoER%8>It2F z&)8Li(W5j*ea{l_v+|oeW<^T%%{bNhFjDAatN0#KrQLJ>JbhT4P-*sBe7jg!rOsiJ2$ZiH=) z4SuYXDri<@!LfqlEX!T>aO>cuI3tuEmP+9C_q1~{1 zuB_dw;@+9k>fU=B?;PN6p6q=#eZ~@lj*n&^%$Xj|nO@$=BWUfU{P@m7OJS#+f~U6H zO>Zmswtt?bB)Tyy!MoZrt{_13T+6`=hbOJrdZ1VF)rl7mi;Gr%7LI$>x3x2SlC)uz z$m5H%ents8e~_qJ#Wm0E-TECDmI!s8Dsz{xKD|1RIrwGj{cCgfiU*1Q{CQ0*>#f73 z-u!!-6B@<${;5gzT6Ag)<8p=qW_^46v=9#6OZ@*C@@%*|%%h)`9C*Ju>RQqH#mVix zk3K2Y^`vP@2Nh2;SrC6MKHiH# zTU=GSyyuN=(X52nzmI)Vxy36(xg)h@pDa6Mmg`q-n5nqZh}+FF#rNJOJ@F^|RSK$N zm&wO?wS03Akv_`kaxP~rW?v<>yy}xrM zGJW`VOjnEZ-4#*4<4xf8_v?%<%3Cl_{GxwNEqbZV>@#^&0#~0ZvDB;-@aUF(z$7hb z==b&D#clFE>phm2h8-5j74Tc!w7BH;*O*yrMO>66E_nz2(m!UL*?Z^aF>CD{@2ih_ zqCy)N-O_z{aJ$C2+GCub?8UjB*GO)!^f2AO=FLSBzdO=S;mozMX)o`|b>(e0Wq80V z_fwybL3d$r&XetI$>mEXd8aX)dzgN6ij*_=^h{n3({!t&(JvS-|1El$YUOSAMVQsVl2LJV9NnW`~mDhwJ|tjvV%{7M`KlAoQgKnt$uT zg{aCyC7x6Biq7`mn7_s;R>05tn9vFR^y+1O*=CmmO-KWA}6W%{>7 zO9P4Z2l?`6h`2^Qyv+VX!i&Z9R4vzW@w4-Szj0lY?c1$euWG+$!{n|v*Q2=GTNtKm z{!BRT_Ui6jvwCM~>lObBTK>-5&Qu*0-&;SKe`(fQ<5x4v|5lox++p74v0?gE%d@>J z&wSpSbfRzn^64tug-)$MR&8pne#$oS^Ag90zrITrDBYUh%=n+dU)gSRWQWej#)3yB z(%FmbS-g{z0^0 zmv^(%#4_Gbe+wrh4IC$3_$E$}EZ|@7WWSez1 zY1`C4`zK3o4Z7>_)aKVwwy(wOrq7$x!2Ut@{K0#MikTtLdu6_MdoX)7Mt{EMd-XT- zK8EMa3r?+!*54zy(?{QN)#vDf(0vWLk~0|hAN%vq_CS+#oNn3V{y7&U!geZ})WmS9 z%K0lE+!NW5!M}XgItGRa&t{d zd4a|fsXa5YdsZnrFw9BlT*+a;_#^7k)>ucA^~`-;<~ynn#Lvm_5_P%P|GUZLPKDOq zl(y^E-IFxqgE@t!hF+InBe!imzy6&Ag3N*2S?9F4PktP!x>n?J`^q(z2lf_h|Mxsh z(Dm)Id8v*Y0=;?sGqtk*_BmzG_+GC4;~wL~*9=>4*KJ8W5oyUeXGXbOTdyEELod*mIqCYIi zF3RR!@#oX>)7p2n&)(V7dhb_FvfC^cxeu=Edh74MX6CuMZq?Ku=ZudW>-LaU+*N1Z z^z2$!p;uMqbaB_sYtuS95>FJn$4BOhc$m0e{#d{q=w#;dT+HsxY}GGkU!LCexrnW` z`g-(E#$yo`-;(r-=5J?C5|Ep9&-G-<9M^mIjn}=hzVYXYQ1G%XHvV&%C!Z+q@i0Bq zoi}IJjb(4QpD}xB=9yv`ZLNL(W|K_QTU(xZ-&0jPxtf2@wIk_A2c87 z>)mYv&Nn~Q6$Wo?$lEgEdb~u;1l`vj*~K5HED&sa$~(=A!E!Ut^&{oW|1+%feG-|@ z_i}SWlEGxNeed7bJ$=}>vY^l*A-gWUevfx&y~mM&$)V?ZjOKXmuHyH*w|V92b@yvG zuKb}>_#;`+RjS_bam@qK>Gz+fmUeg=d)a+UThyKvT2i&wsy3{x%RUpo=1yaI*!0}Oh!BmfjoW_j z3ECt2-Y~eh`|yW;9xcmor;E&=9`Pmg7PEh>Gi(u)QE}e)Z;nIHqqF_#7E&6wxYM>W z{WFeuUEZ>G*^8sh2Ug8^xbIhr+UG^QKW<65`)P0P-+7K-z>Pog(K(YQo7_k8^9-sN za?UDQ^!4L}Cz=KoMrNJ%G0q!~FZnfby;@$DK$~yjJxxBQRhMk9eRg9sn<2h<;yJa( zfLG>bg*Pw6_ZzXQhFN{iGi2#)QT09H=X1kJo7+A8HAh%i`^hWsHHwxmZ@ltz=BZ8| z`73@sSNQx+TRk^t5J}#<-<**>Y(oFXo6|XMEpm>yv0HKZ+xh#1m*j3qIik65-`Pju zysKA8ANy$_I=4Cb*qlA8Y`sRQ2C=NuY@gg*oM6w8b~tL0O8iZUJDfoubeT8)Jl%J# z%~;N0>N9~%x#Ld{d93pKWW4afu~w$lB7X}W#PW6-T$jkbdc{e`^q_3V+Z}ViHmyHXzM3Nw$g|FjtKiOki;L1xy7C># z&u(v8drIu&Vv}1^KUW@Ov@6(i?>~d3L7(}{U&ow_-p}l5w7MU4x|ku_evVJ*>JzGS z?(Ak+*1OBUC*kR*-G|mbeQ@rdnp{`See+{p>Seox&rFS8`}_0rbAhYmwSqRjieZ!D zklXbxA%JmXQepA6kET0WGB$5Gmf*{q@wP`gU{Usiv%HtsxzsN1=-|yv)~{&D2>e|% zbNRxz_Z$*WTVFZ+)Vu$=ki!$Js{agDCo==GzPho6O7C?RZq3WH$t*u#VRj|R#=_(0 zoa|^#%a`l>j9!GUyLeA{1M5Wj{l?xNrB?f5=8N98nBjF?`oLQjxrL3lFaG#jJuSGM zJ5MQO(Hke;dY^4CSP!G;-=i+Z;pvO>OFif@L6K}#LX-3 zeNdJ9ncTT*KmReMiFF?Xbpm4vG%0PW;);y-J!Yzv6$WN` z`c>(tTh8u%8p@x~kM-jgUDwAHOy1QSvhs?`_4neMZJx`rCGX)6$IIK_R-d>Np0dwk=@+k_&P6uYl{YP5>v~sMAYHJA zSL{RN8V+yQXt&R67MPfsSI@|MJM)0jgBRA>j7t~xRvw%3IVpboB-=YqNxy|kKi%9Z zd~w6RwfieSC9$~Qa(=ibF>d1Quc`ifRF#)3C}4Qd82ClrYx#SYK(QFrEB~f*YU{1~ z!*c$_!)eN4Y;pTro^8_5xa0i(;hTguq1;p7^gm_GXs~WpQ)Rd4*t&Mk8;O-7ir&`$ zVivB*v9WmN!MJ*z_mxAli4)(_t40CUmpR?XEHTxKsXYb`b z^=$Fg*A#q{l;_(|ynRhP)i!(Lk3;i2RNhAHOx?8GsmxW+itp;h``X|8q&~=~Zk0cH z{q`|dL-FI{jz7-+bdqQ+`>W?TMY&{C!Mr<3BE8!p733aVd)7Zy@#Cy5zgydEy|0ML zo$N3XxG8w$xBfAwC9>AGk0iHDxX!S8c7yr8gVOeMqNm(WHLqvW4fgbl{dw5AF?Dt3 zY0lNpE97Pemc^y{?(WaL{4F)P+BfrJyYiyYUW?mJ8&*8HYWwrp6xZ#E*83$Qd_}HC zAKN&I*Hi0*Dfi_rnKer4e09@>SQu^`bq-X}#DS}od_&nkGMCdQRLink=bjYmP1w|TL+a3t4s!Ic4_m#-dCNN8WI zYyRe1Xwns9F{djCsrbfosQ@^<% zFF7Uc9v-uQ-p4x`qQBK%UQ<4yar5C1pQf^*8mlRCAsJ zBR6GVJ)XQ(^;NewTRc!#=U&;iZnmbcxl)N;}&{77tIo{{VQ5-ARAOz zF_)o#V^0!eu<7Tb#Oo)Te;?broHgy{ubnQj?{9YBD3_QtCo62?r-W%n|IFE zt>E`34 zBmNDiuS{>yPh1m`(Bb^i_ph&a&A02Ax^QYV;;HQYfm)WzGlIVHOr(8@7m-q z&bKjQSsY#arD%Smrs#ax1P38E5!DMn8w4+3`*yqa98>k?V78nBm-+YGIKF;w3@i=N zjLF$I_0{)amRWKS_sUq5OE3CTvqL1%NT<#$Uo$^<*~h+VOpDZltJSj9D-MLPb`X9`=YqzVL;WK$0LEMZhS zr~k84fl?@O^&X;@^k4RhX^hC(dsc%g1TCt0lgp7uLY%k96iK0kPS#A@r}83~KlU4C;yi=%h(&K79} z*UpE_MGZG;S={)br#$tw;1!YES9Y*)9t&4`$lp_4;^A;-UBQ9gU7n^Fn?kkSX1|cF zd8F+a$bY@8CSi5(&4s_Li=y2dfs|jOsrASl;sjV^rR`!dsz1 z3R?At8C7LDW}nvMTp4qa-}dMCZWS&K8$MHoa<&E2rp@nR%ni1da$JtuE+n1j5tu4FK5a(mypz(3PdF9c|S0~Ol4|vWc z8o{-oLi*2xf=y*!(pQgc)m->noYf~nz{}=N(z27^*K<}Ins{A5VObaD15P0z}-Q~Ho2CwyJF@ZqW`)TIVv##B5k67ce-s9e! z2=#=;KJ(_K>a}p?{cMP5cq7QSHM!d_#5HseZyzhG;->b;&mVJXnKhf0uW8Q|ITBFt z>382OB{@@*Cnh%R-&(>lu!8dTrnmpO$iR%dR6iW!IhDm(6&vR@$q&#mo01XX%B6 z2RU+);vZWtWnQhhx4FCS9RHlM+Dor$EEWYgitqinCU#Gh+}0D1jSicA@Hf3D^eSP_ zlHW(3yq|U4ZTA7;SADnUf7UAV5?onpbYhO`yoc{=KOb!2`(yrcf)n>e#?(DcBuoWLa&{>^vP&V|pl`B-%9VTJFEm23U-E}uzc`o<|PczsKk@0tw<9qfI0 zrcOO~bw^-@T#Td_tIdv|M!zPM@7NaUAXW?S0(hRfrcP_HFtk^|i>+AXi26LQKc;_kb}zYT&OP53LdV#5-LwAuDv>|B{glvF>L?%d5jZS}ga z`Wfe!J?vcX9^W~8rrL??0rkEIf@i1auikiMdBxrSDLpH9K0Rx7H~ziNQV;QoR>#=1 z7bt#7dSl<+uba=79JyzLTerd^KZ9pyfB!h(Z2pl|Z9@_l+gX!Ke||h(7NO^2YkYQ_ ze_>z3tRgnuqHC98LJt_7Ddy3t;0t#X77v?0siefp(8yFo#O`;!q(raxr`p=&3{AJl z4-1d(3-w)RB<`Gj=#NCc_oi=m=ImVb!s^(X4bd8Wy*?dF;~UvGyk?jtJG=2;--oH3 ze9wMt*08<%eA483&zOz4+?O-QOgg5hdwDPC#HEZMF6_Si`@{rZWw&*0iFYq1eEQte zDB#fBuzRMG!*lgRhRXw_85OS@Fec-{W&5s$KhMSquR3N?zp|9-nGWy?xl7V~Za(aV#+ z6!x2Zb$@&49zz+!z4tMHpWkHIqSAb=KjP~~mNLCpu{J3ig{$|@=l?!OI`op%yt~f@ zIDSN&kCEl7oPCBhw|1tY@Wuew*-{4jE7-H7-hS3N5WrQi$6kGBK)DR>vLE3Ug|cmP zr_Hi?dv25HvVRwL6;w7_@U^r{3hsEhyW-`KJexRO?Qb&C2|>&T=FJ>dW)=*Y3XHdE zUiXwq`8C(vI(dv~#*GrwgGy63iS7Plv8YRR(SL>`_XJk1Xi!`C_H@JJWSte0%g(>H zUbRiuXGxK3{b7M=_m;hx^RVV!#tqwWJ;v$3p1(YlWXBaWb9+ zXCUs7RXbz7);dl9S9#A0%xj<^`Z z6=3t()`0UvSHQaOuM(^#ADDM%r}8JpPalu)Y5Pj2Ox6|9HQVrD$3KbFE=MHh=9-nS z`|MotJW})Ep~vgj8A`9)alH0bqW4{uI}2B&WLxyziJrBh=vI%%$=h4+xUcj7IhCue zaY1_cuTxip-6vi(s`gNp_bKNv4-=ki;BFds+N3t0Vd~ABViHcwHCrbhVX1guE$(tn zi=UxsF5}62jE;#rWA@afbf0F-EzsJ#AVyj*&*g4_Y~EvXXQHt0e}>4a zReVCXck-#3Pu&=DXGi-^f>!t@8#1<$&FBXksjh5fOUFxxw zU7?zk&eTgy%G=gQn2A^TFM4+Efex2{{tVN(Ki0`>w~E?rbgv3Kdh6N2LuPd`c25F# zdas!*UQ)nV(a4hAIhBih*VgXk)=O7Ed3f@TvS>}syL5(S9|ZqS z7TK*LqWW{=k3@$Hg$w>>{_@%Ey_Wq#sBzBa+`DqQbsx4SJ+Ue%VodAHU!Br%_pbA| z7^jII1xE$SJEne1`S#+oOxl7CrAuBVA3OImMnFh`uU#U}>_W@eLj`k~vwk1zy0qfs zwNGa+ay<@ZpSbl$%}&kBU)eQ(zQ1Fhr}pJ-T=SkKEfeJUf1jTGqwvO*z^`UW8}EiM zNb2IR-Iu_y?4r@F=!d+Ac2~^Yu(Fd?n&H^p>DeLA+>h@OTohpL<^KbY}OdQ6G=-Dm5P1 zyf0XNsn>$n-QS-0GOvgbdM$A@i|^O=yu3Q+#q*+actUlTt~+?IZ~h#{n*{-v_BLN* zZMx@bwyXbpmrLKocT3MQsaEj5-($G!+rBv#3lw_XMBdI$Q@EmCa`)HI*L)&8Zx=s$ zmDqIhwQt@I!=_*?Aelb>#tt_zJeZ& zy|Mc3#>}1#FGz zlj7vl1RK>__mw^7S5yyX3FBfd{V?m03V*wNU7uj;!|CgPPs}S7e{5;t+i~oVL_9-) z)9?P{6KZn*{z+(doxS_WvvvO&{J5rZZmMk3xHo%&KX((m)bz+bBqTVfgl)Kb7RjM(qRAl~L zc=^ZCSwHRv{ydu}DU)p{Rjf1+ZA3SY^h2i>ys>k_q7eP+Dc_vdLLm!w#!fvQW`RMUg)3%h1XG+kPxH~Gfr z$6S}zD>#(YC<`eHACc*El~m;mnRB_%(0Rx82iw+LyzWoRewDA{ z)^fPWPM|$NxWrz#~n~?3*vDb}~3j46?6DZ4$eN75uS zksJ1YoE^=bdlnU(wH02zSY)<gNXFu2|xBnhDtNBbnkp~pywm!evkk&V$h4Y>f^X0XL zo6Y!+ifwTa2ChuylrUVfMVUBoQ&+?hFl9L~;~{bcU*0|yq* zvn*s*_n2J&VumB*^6dAHes<@vo))38mlRoB3KQIDZ?>jTF68^~vt`ljTg`X6(By=}_=%?ci-%=5K2)~%j?)rfwv(6azt6ehJVR7T%AfJn!M&}IUVW@Qk+I~y!w&0J3=e|* z3>mIV`<1?aeojH>HHU_9_WMYdIrAb--f!pLeOK%CaW?g-Y+Kl$PhD;qpmIUZ*MBkR zsYcSr)Gg4_T+b7vS0=`*-`=?F^2du}&*r-tNs1PW792{JkJU(1oBc{Fyzaii=tl zgZH#_taM;}8ha;5$e;0kn_HN)ocirXksEIcEB9Kf^s{i$1yc=}5P;S}Eau*D zW8lxzI}_}sc5GhRgNAD@ytB%dTOZ%^dcj%WZTFte@C?0v{>UL0zWwIwkJ;D;yQgyF**zA)u3=y3jHsh@6;Tr4td%~Bf)*bqr zbWvTodcLyK6*CU2$-fQFW}Dh~%6+{2%JSyg7j_R~JEfNH?LSt<98%xR_v&Qex>CND z?wbZ?3OP^gYs(T!dM7+wraU8dy}$KA6SiOb^4}b_*wZPt{-_&=dB}PTlHk7O%zp36I4VE|QXz^C>%Fq?co5abaGf`~KWp5^auM zH#$!wxVHTecfWbl@V(Us(bC(ULW~O*sx$W7DH2iIbNp3u%W8k8+OI9Hi{p5A%P^*x z%66P#IqOmTb&0KBUVy@ej=lXmvl&JR4jQ)Q>E|7@TXbt_gzX?oN(jU>g8{xsGGy)!wb`IcNR zH)M2IikPiP;&qX@Vdi$(`sG=H7& z%DHa?w;$!4yk+&euewJa4(cr0eJNd_s&TuSo_3%?VDRdx^PXzHcVk`mSm5ak9~S{t z=E>WaUyE1a+QWQ9Hs#M&shf8bOzz!c$aaVcWN0+aPdu4C;bQvHA~9*+>)-M}Jqqb6 zsp5%c4-xO0UDWmXg@03Z*Q!~H?>?=ZH7Wavf9sx^!p8YZeCjd}%ovpa3Qsh6^jXwv zk@Zm#R+d7ywqHl=F2vuA?g~z7lKNmI`!PRN!BC^UutF;J=E+$}DKB^E*XJ31mDamn z@^I#TzqU8KcotWgJ~(jOCibIYl+ta1f+?Svo^m<6-9D9Fb?n=_%~O9KO?tcVTj}hn zc+SHu$97I{D!x{BfbE0(;<>!d6Hm|66Mw%ZsiRx`v8q1r zwgge*rw8U=WQ`Uka2OJ{j_N%dASD?77)=TDQW7BEGnZoD!Oqb{jWd zZRllOXA^k-QbL3BvKLD=Cw*Bc`184_uatGsp{>?CIp+DVD^g%+@9Vqz^DsZh{^{b@ zEB4oK*LK@ll;AZ-)Q&@ayUK&liw`Wa&^lb^-p?mhT>Y=KaW;3E|t#Q zTRz1_C^Ps!!yI4B1)?|SBzUqHH2%)kju4t8Qe``%rHJVZqr=<155rd8oE7zMUreSz zbAtC_YneTc&I$Q1&bqeGv0e0@n>~wFIiU6+i|w}7ir3G%*!ITpMjJ_2a_?C$Kl2>J zNAZ{$b|n%SLK)wVJwL`}DkwGIx{#IGL6%SJyCDa2o8|X2_pC2nIyL+4f!Axx4w^-` zf0^HUKzOEQYt4+=GIJF2KP0VCyqu6f`R7>`4%R2k z6AB(o+VV1Pao>s+6Y})GJWB7}u5?e!bcs~PeeHYJPE%H{U!S%yLG6H$+FiTH3YkW? zWV6joCkIx)`z5)=QmtwGLAj54T8mm2z1yDo-d)dU zuF}W8v(=4^*{&M{>zUqjWGe+^I2qr+$DFIMN~Yf8VWHUagXL57U6&_T-d>w;X;WB| zw)1!dr(*BE?mu_*76b{(7TbvE z+^d)Oho1gCE5U*N)r@LhFL9gY-+heUWv~3L{zt~POLhN=mVfSoi^ZH@tIqn}7H{VK z#DHPPpQgVlr+jxWv^vK1^~>Lsy;C|vSJba(i{W5!cu;@+PSF%gJpupIoBuQL|1sBW zkT1L&p)*4xIKpk?!^g1}Wzt71w&{KF)>k;}=<-p0!`I`LFF!=wELtmb*dT>=)*elWc{HB=!ie?p@tAy-?7v z{9W#rExN1xgf|O1Z+lw&_++QhDWwe8g?2_$RtZR5zw%tlim$&<`t6~q+h(YDcka{; zx-q-($nL|!u}r^DcWzA;y;3e%+GumJ)0F#C;l#&YOK(lCi&?76?BV*+K-5aFm5+Ua z%UzWpL8;K^cY+$_ z_P=bK)fm=jEkB2!X;#)h@$)+_x>T8ed2{f*$NOpXK7Gd(dohF9@> zL^X^jE)w7Kz-pDYwO7)_i?Pgd;*U*a+qEj$7dP^CN^L4jlH4tL>FW=}mo?kU5@%$+ z^DX{!#%KG}xPa>sURi5qFBjGhuj0+u7Lxaq%Ri=987AX3NBg7Lqs3v98?R~cWnPkr zy%TqoH}K=1M=3K{g?N|BrD#tQnU&AS8{Dh>J+A7r;G%4Wwr2{}QpXzjm;XA)oOxx% z**-^wKrgv7dzB2!S3TKc-|oj9DJ#9Qewo&8?ab-j9~(5o+e6HoLZn5d?cQ6iiah${ z@RAIthWqYj$Lrh}FG&`t>^XSv;Wqtz_YA=YvG%o(DrA1lQ}z20aAmOaE_iQP6O{~7M}71Y~2IX0_-=ho)tw@1V;{wq9r zu=H}p7So&eS|0qEp=Ii*_gq}7KxKYzu-rcO6WQS}-_c#J0=)QhUDK9aQn~TkvHM{VUN)VlDUTX zSyhLV2ZT6vb}qXDx}>?ySfD3byTbqU1ktDkA?^0c!ANgL)X3c%^{p9Y8&2v&% z>)g4!OIU6c_V19gm+)Ql{DFCYD38cyzfB2&0b8F5{ygqb#@}lA)$?O#na+!8Uj&8k z#Kk?7YEs)>)a7+Mli|By=noC^390)f%5!h&rb~Eav8#$d-yFHrd8_%3Y^lau{I@5F zab3+Q@w`5xJtCxuYraj=^Umf3)%3fkDhv4zwOq+5?E2W!RQYaw1($bt&ECt`MNOUb zZ!gPfSur)Bj&DQSE5>D4cb$&9{&V&Yw@VD|A6pcr25)}J-;-UOFO#mn!(!=#OaB>m z9nXJwvUiiZ;UkMB{~7X^wN{)E>>%C(7#e+J76gd737lZYYxS zQhLt3;#&ptoef8&S$P}!uN8+)(2Ob0p6Dc_`KfHj?4~`dHw9JpEnIb{=)o@IFah^i zlLt?#l2`2A@cgxwG;8?x3ggs(ppd5Nih)co*e|a(*p+(xhKSc6i<1gkC#_|e1f{cX zUUR;=V#zQ6$$J(#*_)W1eR&|y<-vvSbN~1@WGKe6&Mtp?c9+#TneVnnha2?QZmP}Z zjhN@W_&Ix%_LKZM9G@q;dt~<)ng*x(tl53yLU`-*gP&X3OWq#nJ|MA4=W58$d_PcHP z!gQ1Gy8+XSZ=RtV{(IafbaKs4$oTW@pr?p{ybMD_tGYt9IPYm`@1@pqb6$CE;rj9@ zVWr}uc2DEUvkWYPTn(Yv?TEjl>EX8W&k zIK)I}T*ec$e$T?sAM}1+Pg7%YiK>xjJ+7vz|&_nQn39=EH)w zFXp#jm$cGee@^M?nzWP3@kS3^bgZ=mzjnFuIh9n@75!((oyg~Q{aCD^V&#oF$A6vV zzr1L_`R8k%0o_wB+03cv&a(TyzFk+Or;78yL)})-3wt^z8Mw;SIuzCXoBr-_{~E^| zCO0n3{(UT95%b2$>rOomP>ciESS3R;U+&L0R~=W$ zr$2mo>RgPD&dtA1@;J`2+j%!SAALOEvyo%v?v2`O>rCfsxBH2&3F~Y7eO=I|-nzZMYg_-16DLdsJpM~Xz=BBLFxIVRh)lqC2X^Z`q;S7SGzE{hn70LG?_v9X zk40$foHqyLJ|DTI`Q+S(=34gS=Os>DonW(} z&hXOg>~9Z_-8$sF>6T$rhwR#(t5=rEt~NfOug9qRbmf6pdpAp5n{{jP_nr^)RnpI? zAJBRmr}>|uKqx?ZQ>mMqR8)ZCzI#R9yEskcwiG>K58Tvir&rokEV+00&V>g)y!Pjl zO?7Zp;(ji$p@UoGY~li@Lr3k-DVNzaW^z4zy!q9%4+%GPN;o#C{y3ze$`b4MOmutN zrO8WY%!;WB`DtswW}JGc&o_+c% z5xB&iF;+@#+T7Yc2JOI$XJ4zcY_6%vXNll4OP>Bv_Q)oS7$K(?_Gi`aob+{hRX?xp z%6(8Me7rVsTH~z)`|inbESY8)YN}rMv~`i$-2UHP7gl|nQ&XF?EJO44{q5$DgKWMv z{O-NoV9{nD?l9@+qy<6F-D?xFOB-7!J{H)b#b+O2IGIUd&->@1LfpUoJ|=$R_x#+- z@G$PzHFhDB+ON~zGHtoIm3KJCPndnvnI+N7mG_4;dp8%F#rrS=kWoO;*O&CfmvS+1?5yevUE!C7ol zKuuag{|3~UvmN>1iMhkB|Q+|@B!@$lWx?Q;Nis)4t`8|H~ z94;q+wKMS2p0b};oTFp8gPp^dvzCgxBN+UjY2Qjbc-ptP>ea&OGwv;1{HS3vYUxb4%V$10)aoAibC-QM$N zT>MtvQ7L-(#7nu2v5lu6T>&MSQouKclJxT~>y(sRA7YJBsQzkEpa5t%7>yZXqic7=f0@W9v7X$5CbuV492 zM^)Tm;e!45idxeH%w|22zh}23aC24GBy*`SlL7%o*~|l}J?%B3hXi$m&s9D@Gdb&7L(gab?LP!F@A>#8%||=G&zA+SII}O(5KH z^ZM+f4`=(>uNz} zY<}3IUY8fvKP}=my?!Hj>BC$ZHsye?jHVZS`3hPzyV|>E=V{6*GL@fO{+J_hOM#hh z?%xE#6rbyhcAPsU*D&w0wzhZalJ=^j42&X6@~sM8tOb|7&%bl<;LD~!!#*u7!|U6`~rTEtaYoHahR@x^y9iyno3H=nn zpBZl;on3xHZgs1>0~dRZY|h8Ck>>SL#tGUVN@|~KI;Y=~DoE9HxE}NH1;h2Wdp!&0 zIb3Vbo^ohu>CVr_c}vUQ$sa3NG&kDpxnAcMhvJLhZ7o$n8YO{L_9c zGpr;g{Is9Wskhd5R-KArz{?5Ve6PODw0Tu_G{5|g7pn{-|CR!`EAPSD9BlT$lW{WKg>CuOi05BVFzL<2fDhyjdjJn(-W9<- z;i7N(3Zh>^WCTpRxP6V69X0#fdRH&6pl0t3CF^RP_Qy=t8W%Do@)Vi9zMOGguv@0! zPl|{a^W(Sc-Wh6Sil?_8RgKlkJKXx)xYIO4{bSd{59T$;*Oq0g^3QSI*O%kK^YYKU zzbURP+vN(n?so3KetAxoW?j2|#T0k1u0%iST^$z8i%(=`&6&w{_t^SPbp|PO`R2dw zZdAz7_T8g+AX!df%art*qlXX8ZGQH>O|j$D^yT3j&y^=`eDq6F^;gil^XDT1bk|r&;Z?qdfNx3<{No#_(S-0(&{PctUu*Na0{Yj&1=(@?WtV!{XZ(+xMg{@N&#o zTRbb9k>iKfkEGsJH63c_E6rY*WEri5?B1KmYtE%APt#u0}^Yl_cZWfq^x zc+YFL?CHgel6Zu(YrdtjSc+{;d&hRjW{zW}&9Q~d<+0!T`wVX!xj23Oy-4@z)#B5> zq`p1)sP2hT8)w6t{g%gUX2+&nnA7oAZj0E=SG-#`@(GnUMCcX-bunFWUcZ>z+o2%( zYoQ~%xb0(B~_b{pQthStZ zgR9u@{WYG57VDK08l+Q7cRfC_%e`xMQ{CtAizUtV`p?z-dUxqb_b&=NeXyHLbbsGY zgNZjB`ya~5dLOmh@UTe#a#u*lFMZvEt1hlyS)ex8zo%NJM9Iv``<&L}?@4F>ecsq7 z_wC5Z!ogg;_$-g1yfEw$%&5o!nfhN2#b5Q+d7|;RlSSf84CFt`aUS$Ql@r#d465N7LOad z8ZUO-{npa*zWTekx|6}KHcpS^N^7H-{@JsRw_9*9dz;Q`dvyK$IYZ4x*^_&;f*t?- zwftCc{MT_SLGhiXzYeyZ-BBkw%X9m~z&nSw=g&Mb<5WzBZ|S`#wxdP7vkih)IawcS zRC(0vEcohYMs;0bNKohEJeAwaE{0qvpYx^Ld`sBkn_XO&gRaL&a%~Qd-OpEXPNwOG z*3Dn17G!C?%=4Vqn7DX`Rk+!wOs2n*s|AG9N|_z|uBHEHXo$5s#xNy3{n1+s&e_``rmz%4NB(yXGk+#xbENR?Jq0NHfP>>{4rC@ zy0@X5D}-K@OfGp?x9gNB<97SQTh1QdC@M5lSnpO+Q++aPmVlL7-~P%=QX*21*usx^ zo$Y;fzc|Ki^5mj_+go3qVEFqyTsH2yBkR4tCl>XXPP;Mv-sjDkW->#pn7 zkyd?iR?O|i)8KtY7Ds+_ys@z=@^*;Q$h}bZCt=#W#SgB(m;K^s_2BW_$3lWPZTaUP z<1oA6$7>dpBFkqE8_1zPcxI9${b@X zH~U5T=Bu4Thp#06Hrn1`_TkUYSc~Yp!WEM(mYHsInIs>oFa26TpF4U^+S`i^8$>-n zPUO1la&P`?jU_B0lWMAGEn6gzy?f8|igg#9_wDa_@ItdZbn8r?b9QVwOh>xU%RLuh zntg+fNkKw7|8&pqIc1txj|42T*~{l4W1G5jlR|)5?v7mrf;qeYzAU)aE3!T2%#>Xs z3x93++EXoUXWVmvqi^1ZOt%g$qi>aKZgu2&$K9NyopPu5Al-2V)g6g zIbLqQnP2Xovo&&gDO;N||EEUI!YOZxs;uqKEbEwWKCLD>M8yAblbzAU=#oF*w=*?* z`qr5@3OD8Wcd_YeG)-E#cmEt0p>0xo@++3H@>ZsL_OF`V<-pJQ>*!imC&$2pmlFG) zzRM}ruReG+Jvk(94o_Y$@3y%+WRjOIkc_+0vc)rM)4ITi3#`Ah9V$qgtGzCJLZF}U zhNmah&M{aT?%~(hd8fbQRY=c={%_Cy_P>uz;I;bwUW%{N=D~J8>tr_$iABe^NIE}o zywk&A%XBmC?C-8Ms^tv_rP@rrSvTc>Z`tzu$J`yqP6rBDJ>K^C!l?pF`}knl<_D?; z%2Dq{j@}ggVfyZ4#~S9^d0GW`l+C1VH^d%(&%wZGCC$sfoke5*Y|(ccMM@5Z++MQ( zamSm5EE2u3)g8TIlXty*ACY~qlgpOltI;b3*|mL=-nndf@AvLZ>#@v`@D+1(DtO~O zUFiMB2`%@7D^5RcDNEjRS2&xXBX++n)Fn7?q>_f8Ra7RGJw>NeB_gb4R- zd(3xV^Jv?>%I_Sjdin(Se3Nu|%@y>5=UyLk+1>MeY-<_VpNR9$Z*iZ#Bx$MM`~|#a z-whoO+?X^?*mCFk47c}RyL6*Ih)fK=EL~(4%iLYJsoBZlkz(TaYx!+!xh1~Njr7jm z8unDWe4&f0VkuiTWBQ9S&@45KvG<`QN#>*Qo=cyfF4bXkIxbG9=jf3$93la^lP2PSipX=M= zP5qA^CarKj^L4vGZR6re1>eKxXynf^G{{#gIB0e5PTQH5*QUEKHh3CjT<2r$oBOoc zto$rs!Zc6g-$$+sDi_s- z&pvK$Rt&uD*UytA)^&y1)Hc{efZeU#YVOPxO*dy`UU~R8$>dh>%5=_-#(*Zi`9C{1 zE>KlIu;cOuy}KJ;PiNzJ<5+I>_(x&%WvO-lOf0_KlHQX0=kAub4Lc^?ky`7qxx=+iOOXrpS1n>$%nAr@JEe;@hJN-XRj_ z%CGOZqOhm;$7UI?pt~35SaL0z^}u_(fN+nNP~oEKcR0z!44Ugp8EbA4WX`p+d4E$t1}@(YsE<2rXw)Sbm{viH%7F24HReIcZ`0WC`DY{wfh2AC>}@WM?p^2SJUzt9&C2)d^y&lQsW&|v9j?0i-nw9Y#60iFgcr*~7{hd2?bHGk zro{E#e*g3cmldabwRn#rpHurf^Or^qqS~q4CaGS&+c$msFr(#_l;HAuwT~?gVSBny ze|*{b#rfqEi@6h4FKZC{o5V2LYyK^XUVrsj;_E&{xp~~W+7Njx!-jpWpHFGRZgyAu z`g1DglS5Ygp0~5~p47r?bBc>ZHk*9ix2veBbJ3x<>ffGr_5(*4k&XCR8LQPkRL=j-vk zYf1R#S8HaTkZAZ)vPsf-rM1RxqYll5nsUckI>c77SEVR5C*+v8{5r)peRKc*>Egd6 z&!@kc&!*3>!t0S8J+nF~>Dv9pQlgh#8EhTS#w^_}P%jnY+yGW)kz>j|DEwt47cEho@~=U7!A~*|B2gfk&J?rdkYd z7e)U#5V$w!m-P%Q)l;qis%DyO2vnZf7|QD3{N~`S6)S#MIcqkoe0JX4oI_su(qqZh z3u=GcODJTti`D-7P}nEyTRe+-?e>kQ|0FH)_bk14_IGby-<1m=Pq*qer#}C2+RVKF zozK#nOML5@RhJ#FuD-fM^hBoO>`kviGj>Om_j$yp2%E1A+ITZ$$4k@LusN?DaEV`D zT$1}&GMZ)E0>+;O51!eHuKbiFn%!1dxr|Xkv~f%Iba6wMzfV>^E3ar?Q*u9#(bl`1oI!3-}Q1tB^K}I+j}50qg!7m>t5Fcza~j8 zfmM^647gjQ+V#`6w**gR&X`jhv*m-%14fH|dC7A%tTkij=Q)NhPG-C~Yi)nX58p4z ziQ-9_it6V!CSKz9-Q4k6ltG7;eJyuyfNWRB=U>mcdf055bI(~GTPUQ|`o#Bkn*~dW zkez*`ie3xL&x}(uw!6HYy|BUR$B~eT#B0aT%?WVR^>(=@vSmft+`Abwx3LMdZ|}1? z&BL;)B{sOTbDM|n!Vl+s!rHza_x$qU#sW6ob%&XbC-EJh$#(tNuB*QP87ho&BhTGl zHZMQ2#Fb<2#$U%Bcmk)qs<%yA5j1HbUubqGuk6`|i#zvpiEPMj`tT<)($vpk$$JU4 z1Dn=7=Z>6NCUxxgGT!?S=3moeVs=sGd%iEphBtlgw@0zZ0t{DMn><%sb3gjWpCs2- zA>H2CFONz}nVn|;IO4#$oN1BcJFOo{b8k+E?j(FWYxmGEbMK+pA|NW z>OKuTTVU<3#bjQWu;<&BC-W>Lx9b1YuMlQjm@~KiGS}B7_eI|Qeb8%Y$Q`d{JNNb<-40Sx5 z*}miE61gpv&lvO>-B`-%W*?Agm|%MP!_OnFud-IO{o;|e>D)9aw|jk2clMUP5YBrA z^E2<=%zl3Qo~qV!pZ9Yr-S+=wmTbxp+|J**ICGi;uU)+M?ddTIweDdh3D=bzpFBJ8 z^IU*?OI3<~>;v(xADb6FbX#%o!leHU@fHpMCM;@BI zytmHUL#1@{&IX>FEH%LlCC&+fGQ0)Bo84cpktn|+eYuI>d`th4gPk(3wEebDzV-F= z=H+Qjj4KUHep#l*JAd%yzH=n{UfDy-{2s2^t$R&AK5lnpbTW_rvuTR=X1^Yvm2bZC z`Nvxw%e)yo_4n_>U3-dz?l5e-zPaX(*HVG-@XzgTf0&;=bkm&gyEjRGrSy9H>pN?t zUCOss_F47%#TFj^@Q8J-tMwX2E-og%NngIDUfT0v|8f&H?WRq(Yx1&Z{AW0F(y>(Z z+WNbX3Z4{7XWrN;CJ<^mDbk+qL*T#~sw$9qX^H~W+1Exh^*&-clfXzg}B ze&@(Eg_grQmDY^w^$gGcHqCod=(Wz1z5Q9@X9fAriOvfy2Gm)rSLzpu8VMgv;971KJFnqLLYp9$u0X?)+kDHnovM`}&2?5+6f+`Ca{w z6*l;4b395g5xC&{rs4NQ=CZ2_d3PT3PIda|{A$n64LRC-`MF;HI`-O$v6BC_g^K~N zLH3W=f>9fnTzh+uG56DM<~LP^ev3HgNKM~k<$h3bb@prV+fy?GS@`x9bclS5JGI{G zn#ZbxB~{(_ZcHl5@iI-HTO7LPw%2gkJeN7JcEz|&bUxpfMc=Hin--&e45 zeX*!q`d#MZ&BfWPzgr&Rt~6J<!v>OlG*0+ z0%y6nJTES8Tx-x65f^PEcSrBbgcTpwo&Eb%&P$fN>TGnTz>BzpRz(xwrERe<#LXvu3>j(uUqiC;*f6ha+@dWj-pYf?$3p#9G_2Kz4z}!tH3s^SkuP} zf_E8tD`a*tCn(Kjtg)|SqZt5$hT zpD*(0F<10*7UAn(FMl|)L}vNnk6-7kc*7}Eq`&KwSVPo@Z~lFAL<8?`n^-TuC-m~; zQ@-lUjFJ{Sy<;hNheK`B?w6BF4Nc-soVVLm#I;dip7D0JzjiVw!#^4bX0n*(2Y+vo z+x_b8`MjK%>gLn4iiFLdeM%{BZmUVy-@ZoB=h=ZZr`Fh<(&i7Gxj&2HhZK*m)nq{_ z!H|d7He6cBxWDnc<)K1#ji}dwe-pMlon&=B^Eg5vskeFIrk*7hYjgX1N*H!A3r~7` zFHA={HdHoINP1Pvdj5;2Ij7!CnkPO-`K*B3?5FRTx0Lxl*^{pv8hiDR$_$&GhL@|G z&6in9tWA{D+1o$yOW7k~&rQ5c#UB!+9fY^ayIZDsFm7{LV|mhh-^a4OGbKQnfj>R;-v&n6MPHXbw7*tV^+1-r`rwJ94~jH|w=*#;yObdp{^x9QAh)V) z_mdO5gPP*~A3gEn+x9x%UqZ^PWH)bEX|H!^k$r{Og9739zHK}AFJ0EJ^DIHaA$J$M zUy#Ylg$B1pZh6_<n$L~LvknMgWy?Os+*5gs1)Zr9Z#>G(yinBGKuMiK^`mZMHAcb?1HVgq+|z|GP7E^Nz){ zpFVTX$PfzWUw6_OZx<4XUCu7hk;{k<1wO#_Y=`AJ5W>H++jf9L`Gj-1ThE z`R592IC-a8&3SKm#Km}HPbdS=Q&r>IpuO^IzZ%XHU!iZ&)9Cu;v2L@+i`Ns*?pV%p zd-lrG4JB_acPeh&a`}sdi((a@p1Fs%TkM`ECakQfhJD_rDhoPRsGn}VF2-T1(8F;5 z^<^g4y_L)P79Nl|5FoK`=KFgN%o}!Wc*tbz{pxSpgDK2XEKd(DYwMWv+vxZO=Ql0| zH~iLHRh;Iq6=Bh{*TTI*y9NFgr{qkZ_$Rf%qq}Uk$Y<8{y4gPx)mJTy z?8%T5(cP1fe3pUNZprt&j4+eejF02KJ*-)4%W5fqEyFQs#`_uD8CzFhs7v4Q>(q?o z4GdcSS5Fx#tyF#B_i@#Rvr9Bt4CF&v^yb??vk=-maru|I=_~>-LT-8iA<$K$9^1X64XSpJDDdfy>j2B$nRTHQD*_8qMYp4OTN3E|*%u zTeQ!ztn9)^b%q&=Y;Tle&!)26W-6(YKX9Of>A-<|!FeAIlN!z~KYq^H*KY35`@vFD zZ7**=xgWQKjdg9=dX6{KD>)N=r`p|ITRK(LLv!iZ=e-ZJHyv`dy8Qf{jI^RdT;S)q z4ZLg)pB`7e*u$GP(*AZxhMq1ZQx>6 zJEZc<&v-`eOTo&k-}wbDG`JpmUekP(x!$w4iz~;oS!BnZDZ>3{e4gF*xVyLEnEgB} zIj295+m@faIcv$2`SR+o1spH3%T3+6%zn;_;zit>D?&aW-m`Ck#dGFEwzB(@80Hu) zS-fKHpCr}{d2=RzL6#TmOk|ASPx)yl<8Z*q@Sf5aFFwBS)(2lYEbhPmv@lSvg1ydv z%{I10-}gSe&amq7Yl(dfD<7!sk5KG$+Z8aYZj<8;=joGwEi3vX%T^q-Jz=-{(Lbeg z7~eI?m*yTdaX!UXZU0VU;$HUOexJVdPLT5RoYm&XmevzkCj7E6Hqdj=pN9*V>xce2 za)~{Bp3TA2slL9|1`MoQ)?b%CVw!(+mk*b!zT}3zvNM+4@w3VE>5IyrIcdk|e>1e) zPu205M$FqeS+eieuh$VPw|JaW_-LrxBOz}dc=!eW_I! zXKi^j<(6uUi8Mpe+>akykIBwh$MXJ62}hm4%Hs#yrUmovF8Vj4lYyfwRGe#zsHEoW z?O%=v#d#=fxcWVE=ZE!60{Wk%Fiz>UDoyOjy!p4Ce|HCGGDFmh+ozjajY@pp+dV3E%0lp;8^tMfx~Gbj#uAKA2IiC`mo<3EmFsk@z%FuhgXZ1 zFik7t&r@kAnxgP~`JCtkb7>LX{|s(QtXi|xo&7#&?25hq_CJHg3p;itE53tca578h`(^ii(&lJOEtwe+CR1v^nMb57WUx5)=p#q=<7)ydI5Hw`IXAmmD=%W+ zI*XZWfr8C$>5jW{uQ$3eA6z4*8OE3S`Rsea6ju}TxqfpJJcYQfec$GIfOEf}SM|9d ziH)I6Eo^@ut(UAkewx*+Mb>lee}-QP2OabI0_2ykv5d@9cXxNQUfq14=4;^tGq0@1 z%{5lW#V(%O!WW-U6Axz)FP$^jhl@GuMfB&k^9Av|>GyVRc2eV&xFq~+;*q_a$%ppb zeH_i{C~@{5n+DsiUDtLUcAU;$ANH7YJ(CjOj_7kHZtpjjvF$$gr&aXPQ`d{Wi=2NI z3fnd+zMa2Es=UpLA$I10=}|olk6RSgza5Ty*4BOA@7#xujj8Tao|twX4b5+~wK;G` zwW`#rw5fmXwlhjidvaTE?0b4`N&P2&3HN`}dCa_-$7XE!Y|VMI`RGOK6L)4NJ;*)W z`FV}A>6G063M zELz)QepXJ#>4bRdue3w|8Fbr|o+e~6haYE;5L&pwTJ!p^2Z7h!9wuDMpT1`9T2blP zZR*L_1RY&3+3yjW@ZQbB#_Yvwrl{R+tahJT$^_4vPFWk&@bJ3nW2ucNyJcUV)?WC- z`sU|pG7DJ4-frd)5>E@;cJEA!k^-yQe+Iv}b@z^3TqnGoqfEnic)b*?0>HO-3*%#FGIed6aX4rYIo=~!C zj=II_dfsQ%>-Vg70}-HvbBcc){%QV4y?N0yoP(Z+t!#m&Wkp`Pn)g%-Fzxcj>~>d7o%6lw5tWBOx5qc zw*9%!{+jWz=U&b4vyY$nJmu9c2CG@m6$2cYy$c?%apGn9q`uey@KTURb~I z^Ym4M=F9#(VGQ2AMk{Ew88UsEZbt;8f_opH2wJ*+)2lNYZ!S+fw8#3K_xcSB zx%ZSs+}g2^>w(AB1ukE8`*(CbXv*7?pU<~(&aGWP9hoCjLNZp?F>_98XxXFjDZeKn z=uz{655HcEa5}mjKQ(=y!AYfKT{jDb_v+Oc#cui;u+4Xx`S-;-M;x}7RQ@=$=Zdm> zx)li!!`y${bTQBTbFq652kQL$$^61hC*md_`|8e;wd&EwUyFID@LOAMC@D6av(6~* z_H{OC7LC&ZsReK5hBEqDeiz-NojUPB^u7Xze#R;tsoh7`J2`#vDOlGe_IQQTzSO19 z`2`+s-h3w5%=%?t;fwc&%kSl`kY?UlYIUzb`-lLk%hV4HNLdtwX(4(4ghKtSejH&i54cgePB~RBwAOs-X+7Zt^F{gZIk=tk z{ny1YImRw^+L1L!iD|0Ij;JFSGM;Sz*zk!#NL+w#PF6&og3P%YOJxrQY45d^m~wD; zp6f3ok50x_d#k^9UyOJre)Q1q-db(H<0>%`Qhmqv>$ql3Ykqt3p3*Jz(74-wXPzn1 zVVxUpsv#2oGlrw1@ZyHLk2f@fL$2>+5w6bdh|jk=y};J&;`iv2lVo2A{Qhq45c$-s^3;kra9B68qXz%`9 ziq)=s&WCKVc8EW3y!d+=oO8@KC0G`FoHAze-4oWo@NB~o zk#`n9o%dCE{%0^y{Jd-aJMa6Q=4pn>L7(@XOOxT=y`O)^RBgw6sD)_ub1q8iybFF#UHFIj!Uu;xi;HilaGQ$>E5RXRw4|X3UVpirWZcXoc8Ng(ud@l zn_d1hp62sDeS7r1RkI}(`yFPb?wG6!Dj0=+&KTQaO%?qapI9W z`{jCTkL)oLn`7mhQQ-B!GV6Vw^%Q^2=*73YG&?fmtP2h-QdyX}tj1^C5(&T6Ton&g z-gLBDo38OtzQB7at7bEEhbY(W>bBYKLZufYuY4_P+2>)luKD7LC#%|9EuUU&QTV=4 z%E5n*DSziS{`(TXnTgXpirNl|b+hiDb7N`X)hF%0PP!e>HGDIhXVL1dyWUUUp2wlU zn)SZ1Q(<3%yiNHr!^z^3i@V-FmgsxXlI^Ja`xGyOhT`6gg5*8t3&kRTrUtf3F_^Qy zX4|!gA!2v_IiaMkMcs$h^ElI8wmdWK2$tkMTfE;;WBU}d!cU%SGELeh?Cn{&MUZt$ zDDwn^+zRgd2OhYxPrA8$;(iJJr{PJiZNJOU2`qA!_?b{?3D=RmNHR&!0;zFX6k=KY?e;we6vu)kn5!e8|b2?{DEN&B0QB zKEKUfo^egaMK>u2@h3lv9|z02u-#uGe@>xLV99RNqKCn4zMB;;G2UQ1d;eHAkIh^L z(QDScwzlicg)$f;id5%(d99rBru3|wkK-j}2cFA$0(TcU2dUZ_a35-owfgYlh{zRz zZP)i-?6_##QFwV-ol%FZ*v0*V=Pt~X%d@un(iCp-aD!C7-mW9-m7BNl79@X|c++s% zD#61UwTX&`veug`a*x?WI`=iJ{W$enUtUtC=K%k_f|P){ zDQ?pC`TBb;OqaD-xy8PWtbVb#SGoL}*%r?noAxEArDD@o%~eTyJ)_rfS^LhSEq|E5|G)#Dz_Ppd zqmG6svERg5->7)DCJZNxfyL_U$A@fC!$>$aOPdu(rdAn#&{$^<# ztv&raeZ-#Yi?qK#G7kPKNuj9>uEY`R?PD+_PFUr3> zc>T2cxsL0rz`FkoJr2&PzvVV~t~Ps`5tnAc#b7cG5 znAUMDj(Xc+7RSB!^PbiRPjk&aB`;jeX&L&j>d%tX?wuglIW?a^rPHJjD*T<9Wq z<_VpnWeb863hp#6jpF>E7P9l;s(b5?6*U*FxOdm;M1vR?L(VUGi%oO;770H*;_-{y zQ*V0nWx-A>1E$EhJCOyI4jd=#cygbJL~Pz?{h9Tt(AEXo2DfH2c*`_D*r_xnM1NzR z;)@kh7eACesoWRCTNyvcMQhvfta`l#nti} z7M%LYzqBn&?CDBj4}&LhkNCRh^)xLyrxm_@**eqvM^w68_WH0NkhY6ovt^@JRpavX zy>pWolHOj}VfyrVegH#L^FPZa!p*7qeQl`>cV;apHSMomvbSi_eyelI{r)%P`+9OB z6b@=lO?&cMghN<=!-M(r4E<6M-ViKHWaau?=={@1O!bK=+wb#LXFa;le3`Cl)t#iA zZ)x!^DmSapFY|V*E~Clor>&ve7*BKE{MZ;Pv8#-+-1jwm^x@dfW%E9^^)onZ?h}7` zsA0DMebYkB*C3RTvd;cLos6!}G-et6QJ5^((7?% zeQ>sDVMXA#9ejs4m$l!#^>{;e!o8h09-k_6__%PL!Evc2YajZhxvg=v)0oK^8vWJv zMd52!-wi)4ZqGcpNNUQFsq0;;qt>MLhh8^|eK6&&Z-w-{T;Iw5owJlTb05gxGwGds zXX@HzR%Y6McS{n!Xz-De+dTQZ>>7b~6W8_KpAN)vgm80yO6(|SdEEB+o6qA!@Ltb9`{@~@0BkxlV z6+D}LUCQ9y!F9KGJg%`mc-V#UimLtVmNiaULLanWcdU5er*L&|nRQHuMn9{_%ansn zixUd$EMlJXCOmQ5{q|khb`y<7rZ>+YGio~!a`Bdr%cKKN$Lr6jbg^h|{atu8M@ltz zx6#+7%1iXhrdL>=Q4aZ(FtJoL@RPV$j)}mNqaLycYfcN=c^jIS%I0=3dSAXfW8G}? zTjvBy+f%A@);2zPdon|v(|1ps50}!1>duP?=A>9I@R(b5u65i}y4{h84y4?0FdbE=A+^`S%QSn9W}QK7M%bzfED8+XC6=Cs&^f z%4hWd=iipjbt9wvc*qecg{aES6&~ChEn-3z#s~L5DawlwpKE^X(=5h(6~&utnB@|- z$6t8N_UX>$7MaEkvz|Sa>cSbIr?!ex0s40tXkk zWR_XH`rNR6C6iK|VHrbh^2@0_%a&f_c6f0xz0}A;WLN%&y&I2ht-DjrxHtda zhl*EjbJ#MDhppgT$bZl4Vc)&p`}_CIV3aAd?b$d%SH|l4c8+DCOxbJd#ESNdM|}Ts z@p1hgqeZ>~n;#T9GF9ob9F9vBR9+Y3(zN=JTj4sN-17Ave(UXfIH$5*kM}#bp-Y6l zD%SL%tvZ+b)>fk+Dhd-~=&OVk74ET5IKSRZH2CEnUcD=u2 z|C@oumA}U!L}l4;LkWXBk^276BV13ioGlMIuVK2x0K45?ta3!Ot>Z`yxslOF44RXHS;3< z7tOLO()##QsqgrWM_fx;LrpiW&&yv_%I-EJs(6}UF0=FgJ>ILfF8ak#;#0EC%7n|e zJooHHkprqhO0}u#+Lg;PIKFqiUg725+0;EtsBpLA+p~fh6ECuzVNtxXxBuz64JsFQ zF7;Kl{dKfDC&%y54r`5&Tf6W6D44ONy;Mw%OTl7~!Tmg6xzZn|4VSK8lHLBjGczMQ z=;MT!JW5LgpY_kT*mYCYMX<{DbY_Xik6887Q%@IiE6u(i!Qc?NI8~lKLdYY#W^(EQ ziCv-QdOeXRd^JLrTs;|L|7jM^i(X9UIPRI)NxQ`{3evEb-1a>ig zianEjqJ}?s;`@l&-+c1T0vbOrv*p{F9VYc_!W@K@ekFOD|NZcz z>cDvgU(YVRcki}tcylOrda1r2qhG1$g+#yY-g&$3Wp(PdRzJ0ybK}yVPjd3&Jqgn~ zH_T;E+t`wg1O1jeb!R} z`AH8uelIT5eKg-fSjV?oU0j^iQU4wnPtBtiwUu9tdm8;V%zO0a%VE)LI_y$MqSif# z)wVhuyMSo}U*ykI3>_cM9{)I`d8LSd`R9&n`C-yl=C?bJ`R@J5uCrjFI+xzh7e`k& z%VpS3*1C77o{ixh|MiClA9cKsZ&S(NlDSc?UahpFYpI>>p)`#x&kvsZBBU&sCOa!Q zbKO(xM3!sbv1flIYx(Iv-(bDt)??y&De5 zZa4q+*)KSxMo;MK1GV#cxy&pxH23L!m|mmyu_J`#r}xqqXWbXy%+LEIq>ywqj+NI~ zxIt*4fHIG1vcrv|C)_`F?)~Xu^5#E-bfMPOppf`^_$HxMRw5vN^4H=V z7RRO6&z)0PwCHW%Wm`U<553GLr!8;9?A{R_YjgPenwzbRE}iRF?@_E(iHk0Nk)->g zIr?K`d84AZLtXS?r~JjvZ>o%xSLONcN{^pcoVcV>??C5m26sQUZG~3L{(X4tc7k`= z)41!(Wh?6Z-#z7U*tg^3&wC+xtV>p%vpu|fA^V{>h2he?#m?)GSDxlC^ZXg#tRjA@ zal-bevOKRDt_p5^V82~{;iCIuuRs4d_(r6H^Qu|p?ClB+x^i=qWS9N=tjc~m@9i~} zGgsDgos^kU)~%hc@Nl2MU+a3)`<4ofk~L{pihVdGSDu>l?f1NA=cZa;-M-<-#tq&3 zLVtedd${pJKkuQQ53#S0TJgC@iM(T0Nt|$R#=-Bin{H{dzcwkb$-n&L*xvh#KUdjp zPWa01`n^|wN6lW-&-ZFK?~7k7G?(dYf}3XP*6S|Ec2AK%#yKU(E?fA#)A57x}h_26JigtHW##YpM3bbKdkA;qGK0c<|zJ2{NfxW+|GJY^pW)I zpdOnyJ&g9-HodMEak%-o$GvDGgIUh)z6CP_5_j!B@qP9Vi`}~)i!cP6lKJUQIuRgZQ ztYDb^Emg4Y;{tgLJr|41aY0X(FWAz1BJAO@J#*ycQg$;x_NgLQE&FY}^_O{fQeW;`>fC+Ke5>bHyZ(sTGrdn$ocko5;X1c{ z`5FahSHo zSn*g^)NRX~j2Y@xtflPvPYbwym1u48c{Jq83R5F#69HJNHUdv>xxnI`( z%;3Rir#Pi$Qtbt@yPjQXV^qADaq(LuyYilW9T(FDuCZ!$9NI0nd&er{C_$ybvcE}o zb6XA{ip#z~GeqsW?=71bb_F}MuC7|IBy(>=K!@`((LF(&F03nxSGKK;@;(_{{OWPn z5dqe9-reF-N)!C+?wAPL&YipRHgj1bSF1>0#;+EU(rbIGkG&NY?+>m&mw!^J{JP#B z--NIe+&A-CU+-a#&yJFoXJ-pqxb*Tb(}@=RYJcYXq_{f7pQw*Kz_Qln`tivUvm%%$ zvFHCtl~&sLxh+AX`Ef#b=j91ntIo=3RnHQe+j#HF5vEmb?e*5j+!_?Ruj~-p<9)!q zH}-4O>?3~f`z+io3b@@(cNdn+^*jGC~0nA&~nFTv!P$_gGmC5 z*Ez26KUca+RNha&t)!v$s=K+G#Bd*f(=+FOYy?yzfk1cCczRI-iJfHsI&e650+c0_ak0&=JY4r=wSjWgyA5?4avs0= z&+v8f8jiWrpB_x~+v0V|aO2ng;d>Ue7r#1R>?^$eVQJIF@O|%o7>8spU;F!dykD`f;bY=CaBad3`IOnpjn5104CL<#+IGs(& z)#Jv024MxZK~Ll)2`-yx_pLelv4(O1r~Mcy!^|HgBJ>lTW7j8GJM8m3OcziL z-@iVdORdUV^)c>DdH&OfPreJ!-1_#%F}>sa^~4!&EvPwDbAYk*?p(P?lk{GG%6z@X zl0p8&2|ah$i+2xS3%d7fm7Tq@$da<#kGuD6+}zHw@W4OQ=96rRXZqJPddxpvux?}C z9hD;nFH9WXUdi9FGuJ?Sr9eUVE9=KEzJ8WT6?kzZO#U>-lB}tQ{e0WkxC&pipIF^) z>F{#-lZ@-aQHP{vxds1ccq|oaq`~ybP}sr4dX_=>+ys91`@x>46_<)}fAx+p7gSwj z<@<@#aKi$f`v(_!Pr3MQ$>D_Q{_FRo#xjd%NUU#jVy}IiB3|~&Ouui#n+zf#rNj(3#{0dq%QU;kjxAU67Vf~*gCy(#fK#iXIx(^;>Vse z=ia8ayQgowF}iqyIZ&eD)%|-q*FLj(oLSnjszmNsm~*mIe)O!%tOhME*Ipc7!?0Oq z|I}ZnQ)FL0-QAXT?qV~;i|fLSmln-hxVo-T&o{8shJU`~Q3YnJ0dLY4~pm*tOb3nes=j{iMF?#LF`?zkI_O%w(DB1Q|Pp+C2zuxid$%Bo@ zpIH4ij}_*@oHWZ? zxvk8nV}f;F+uVn3pB_(_n*O=l@1VoBl?BDR84P}FZtW{=6`w!z%Hjjb@>vT{*Va{@ za$L)*Blh8i;SxthmcGOLr?YD~3ZLZJF-79DU$fwphqC_1HVN&%HTliOLhXQT@$&im zjT5A}7yVU}a%y_Bf7ec4u36DlGla@S&h6SMp2pj!ym>lX$Dy1ldt`UC-aBg2CKw>V z@$%xShnZ$2zk5~W-*LrO9R8eO{wiV46fbs`2boh%KQFGBA~1jZs|T07mp>L=B~-EH z;q)^MhqwG^SZ-nB<(pk`eaQ`{&3mqNE_tNG`dlOP#fvZ9tM8J5Po&HyGY4^U{Eb<|$yQC+v<=Tzwy7KQLUzT*j^ z3uI~^bsv7kbTdy?H)?@N=W9V*N5_>JJ2vG`saoEDc)FK4+pEd`Uydrhak1X3_o2T@ zy@qwq4u|z!-`6!S)+(8Qz*6DUi@7zAgwE#AO6fkgIbBcT5_4_rYwl?q?cMSp-P1Q` zD$4cS|EKuhfvGpw85c;JXE=Rtd?Z$UCXHPWO^sO`SeY;mG}lZe|3|E@bXxEJ4uH~qX=^!58)e#}P?OcFcw;Xi}C z#qA_bh8LN)-d7e)eR2Jm;AYDoS5oH&-ph%Wy}S19f!Kzh34Y-sU*^nSFU>vCXTb^$ zyNy@QJ#^FeoEohmrM7A@$INH5PAN}mIr=ED-r`EpL4i)9G{5&cNU`g_tTI&hc=K#; ze`wJHc8{&w@6Y*b^N?NO#i8;|9M+F)Yz|1JCtCU+k-k+W_fF$>M&;TEM^wV^FOyiHci3C7^ZPN|v~YP{yKlK{`>j=3=aZ6^uIiNb$Uu}=tG_h?+!*~ z^7Whzb-SIn%08VzV@s#NbH{TV4q6`Cs2REWz2(yL>jW>a`^4(*vcmLA-cFBihO$?T znnk&;mfZ9-jD9o6O!-^wYx5a$Urpq7TI6~pJ8ikG4f zzJ7XCad9elzVkt$Y?q8r{4-WZuGp>4k@}iLZc-Va>RipB{m;xQ_B=>gkaw*5WY5mM zMV4w?j|=_#IK9Vq;>pC{1`Q0CgwF2qeaeux+5Mh?*z0C~v;Pd7U#{hD&h)#{6Y(r# z(Y{BnH`*hUjMfD>pR#E<(*JDEjA!TF-aN1Pa%Ahy{N3*!bUNLCaZ>!i_N#Sek9V7% z?%U(@^n#M?nOQB`(Nl|r-&;>EkS$8uYBwoAe#Wzg%-cI=%+a;C@1Aa!c~h7-^OIGI zVU-M=wSldes$|{Nl}{A#X z#qjX9@Qv)}g1v$hV^?>rtg!rfjCa>XpBuU|U)Sd`u3Gbs{q@b}hJ;zWlqYSySoZEt zj^vU3fA0KjJ=?L?u0Z#x{p7ZXeoE)m&TP2%s-bx8vn`=-uh(o|!+f&y_Memh7Oz+O z0*_u>F=Tytw>siwS#0;2?`tN9gt~jT?0sm;Dk|jn>+>A56w$4#cFObjOyQZ&6x0_J z()yq|@j9D;U6!Etj=pfF1KW1j&OB$b{>I9;hfbGg?-6tMoANy3;uf~{QiH}lC-QyS zxc6Q9Skis{TKGZ1rM|E4CUP`M$lP^jnBaELNqRr?9w&9r8zv9!E1uluD7z_t_-kjn z6ZaY4_tzelOND%S_Pd9R^^?=P_3;rKy?j?%eVx3aw(QN(=X*>his`mz)Fx;w2-V)# z_|)*4$t`7H!MIIJ3`}->42|4#g*k?+hVfm!MHa)`+EpL_Gf1v-QrmW+Z1>diiRv~y ze1hu55|g5~Ifi#%u{7Y@)v!T!o8<0|jyZ3S#L0XJaom-8KcRSyNa(eUl)RZzif=k~ z{erhkh&DYwb#8r|PU#JXgchFr5|h_*dP-dWki_fOs?F9?x5@3rrx4jE)$b*)hfRvI zD9L(pO=_a;js+$ueQDQTnj0Q@qha#wF}pHr;^W}r?8fBe5^X=-aCA$L zmO=1}zey)EeixR_zh|rND%P%JbJ^XIUu^Zrc3k@FRK)rOuNArX`E*QmxOU6^ zs&1InBNlz3Xzn@&#{HR%8kPJT`O?!I49$4urJg-9PdTtMhFyDN#iixP#OFlyYV5aQ zX2`skn`=D%pRw@Uv-khLH~r7>>r?$dz0!XDgX2jgV*!9 zXC!H=h_!Xg3EVT2yW%G7m9!@Ec);roH=axE_J5#UcyPj;&?SBC$2eX@Fg~$h+`lLE z!In~wSyGP{bZ{LJ3W`b3EMVL?t1aY8!RvAXk;UxXmn&B2ed2Weo+i9PX?644!~G|= zGM=*vV&7%^MC^Ji*S2%-D?S^wGdJ>GH(vgI!uzc~Q?Ksia^(xT(f`n3R_w>hUpKD*M~9qKPTjW@_5)7B z?}Z%lq~E9-UY*78O2=A1jd$g-`RX-a8`e1b{+6x3Y|5p!3SK5V%EE_ufJv!^xzE6H&^KpV_JUq zKZBcM3QOohd+CE(nF3ehm+!dsaYcpPMwT-3Wyz|}=brs%m?LVH_`R-ZbK2aO**#5- zmRoAo&zT&*_$N_XZT_}%%eWVW3G*Jg>-0fFNIv9wuHma4CilLdIpC{$()8He2Rlkl zH!b_xv}BLp-L1{HHRY1G=(FsXP*IpA{gBfz3z;m$wvJToqQd}Z*W&_$+Z4*$%CJk{x?du8k`Zn*Pa zwGy7T^S+yc+!34OG56>7Y&&Zr8O*Qv<;6P7HwBSSiZ;F+dlP@&5>4U0@!7mdV3nqs z9nvi0w|q9KGHLLOfB5i}g8<{ME0wP=b6yj2PiiiBTDazssYq z56sM8@7u)0__Fg%@?2$}rB6?H#4xaz-E}{DXhBre{>i;wF*I&!D`px~E4xkq8CabKx&Jf#_+H6(hDE?cOg-SV?y|ot#n!2(-T9??boPO#dS{nDoWAFF z;(@yweR={IgL&p0d<)G)vO>C}LqCaP1ePmWmR+cqb0 z$BkUYyOAvcN)r^$#klTtj+wdK`N3^>cE5}{0RehHN_&4WF_`{&z;Rnze0pL}o^F>w z-G&8Lc8k|Ol=S$N5ZmO{ReiCiHTKPsiz^;(TP`au!ysCcd;E4+*RgxaR*DT9KJI^e z&plU0Amr??StV;0c-QXvx#pq2Z2W8Mmh0OkHLbbzRJ5O7&Ptwet5o~i{mzaD+MBb^ z7Zkjh_5Q1Ak8nrj#d8Yx6|NeY3+l>8-@m7%xvr~pyYyYpBlrHAx1Dgyaf&?M&3J8_ z9{bZ6t7UxiuV?)%n%p)iNdHfvT1J=uTxil@H?|; z;WxMa@oP3r`g(ZNnc0`DELOj~##6>%qVl(|INNl4c>9z0BArYdHy$xv?XDTQ{+M>f zlrpW?Ung;H<2~9`YN*1T8m0EtWT(Xots{wtpL6VD6!%Y<(Gn%Vdi&HVlV1Lu>3lDb z?$-Zm?lv6ip&G7af=0n7Qz6+LmSC%=-Q2 zuTk5aa8i;@&_{}CbhRH9+dlV zHb8XIsRrwtS+(V_JJ&W|_))*_!I@LDWWN?Tif`us&)|B5$ea^m^=>vPBOvpmjsHWVuu&wT#=bqThuW|J{EVW{e z$Pp&j#cY3i<@s0`y_hwA{b#t&CS;#7ee&MauC<4n*sh*_)pp&uFT(Jcn%EVFwd$97 zycF8DKCh{LZ7uBB->^F8wZKVH4Q1WyR>vYDw zuqs{6mvmjs@@B>7m}ggb1NP2R?~AI64Qn`KaM{m&>Ha12BcuY3u6VbdFXo-7KC9JX zvE#h46a3!o`}B&}Gv9sY>q_6$C->$v&n(c;m2H@9T)T0@f%g;d97yhx_-gz6IBRJ1 zwzVcd4hBt`98qOk^VBmdT5kh+)n>E zqG`OXsobLS;%uM%-{nhX*QwtOo?xcgq`Rs^YmR%(jz;^msy?rgNL(1PtSS%e}>}{4c9i5&pgLJ_wP){lf|{nvlgx_DWAykwo)xm zU2*BkSqIJJQs@7e`E2v#r!&12o=8_r7IA8on<0>Sj)6JK}zQXZ*AzLalxEw6~$X(-+pSS>Lc{XZ*+*`O zu-)UeI9xt`qm`PfbV<=<&z2Rhv({R=v-15jIa0ole@}Yxj@{D#lG`=UF|ez~wy8K5 z-gKV)o~c3i1@rOeJRAAuJyv^SSHSt+wXURRri;txp8K4$ztyBfJbA%V)_yI+d22ND z?eE?C{K?^uKXw_Xn5h3ezd+{O?H0FY)0eZh+_-gapLLr;aO;EW@?$a`+w@J&#XSj* zE&X|f=Xd=F3zoLY2fQt>@h*}1a!rF}+MLpo7){$a|Ih6l+f)9??K<6sc`j2>z^MF6y~2vK78lrk;`6Bzx+sa z;gb$DT(MNK%1rCe!-b1et>TnF#K@FsE)hO_(=e9NdwETJ_B8`xhg)AR7_cXpX%Xwv9|offhbPawG6?1>J=ilUn=;%xi0r4zQOZx z?RE1tjd9W5AxD}!U+&0auQ+|~+a&$mhu56l8sm-X$H6j>~lA?B)wQ zHQ`a?(qmQ(Ia_a?T^15v)pZ7r0L)eh67&bc6Bwxs1j|6W?C1a5x_= zG4FPsj%bO#PN`SgkzEJ$_8(SDaXY{8`slpP5YJ&mTT*oM0(X7pLFmpyhRSxv}7iNWS!mhZz`cj(cwPzb4Jl_xi}H zxR{h}y&QAa`>7WknQ}iZWMW=6lYqSibCBPxWtYv*zSRC$k$%m)xkaup;`WlRRZFCg zv+0N^?)tvgB8@?->8@bajEM_)wtg4S5Qscq5$Ll>AtQI`*|YnYpDzBqzQ-ZtV$!LX z@q3nB6c*55_~fkag6KuT^K$w4cxHAA{L1;a^YBfkbzAcE*Y^ou=xy8`Ze{iJz%~tr z$=;{FoHtHzo~rjJCA37H#U%f7o2`TrL;dyqbp`@Au60U3EBHR8N>_UI=b9w;D2{Jg zJ}OMx!#~{FIq&HE*zJtDxzn?D6|hfbvShO8j?P-QtV93d!C7vsz4jtUAFMZ@e147* z6Gz6LWztsaY(F3K?LGF|ZPWb5)z@sg?3PDvEJ|Mb_&-Bw<>9>51yTL0&%ahNS>5_Z zY2lV**O&f%WNVS>+bMHhciJiUK)-^52{(%76gPi*wWWE{uO!0`_ia0TT%Slzo|`uR zhis6K_7d4Pw}?}h4{uzeT&^+2h4VX`y{XXVKACLCo)7A}PfyFUT@tsMl=SNFpyT0e@)4dU5i;gU` zlZm+~b>Y~f_jLxJgHtwG@X44?Tp=92`?;S1%jMLz4_B85S;twt7c`%IdPYsoN$wMl z_0b2DLw%mQN9>utZ=3v{dGbA+yoO@)H~&7O>+Kc&qxQhce<8B+Ugkx0fX=x^|WSIoTo5@cQ<{sTLYr%(&&xWv6RD6}|sh zXH{s~s~3M`q&F!j?)`S`#7^rU?-CP3^uGL=edVBlq1xQIzp2t$7r3MzS0w3(?ch8( zFXm_G>ow7`tru_1j60sVOW&g0Gyh8Losf;$Y4#CWty|L;eNxjGUAc~J`|+KQg@=>) z&sQ@1&3p8Z*?i%Gx2|)2ADqZ^+FTdBau%!j+zwHf%l|ffnd^LEuJ;-qz7>qTPv(By z&L+SSaqMIA{Ta9Al&XsTCU53kV5#zUq5f;;_y&`+$Da6H>8_XEkov~0`kQgxnW*X(+z1 zo_|ccW7XeX*6kL2(~bWMo@+7iTW(dgsAuJ_4%`pR{Z(+f4_UY>yZ%KE;w#;Ka#c%}~ABPh2ZpuEJU_QN@I*TIT4>qpZHR>x$0J zeH63VwyNu)>kU>usprfN3#NQZV7I!+p>8fC!}+}AQA-sk7r*}GHG*Fib~&EnuV5_m zx*v1%@ocYUFGaVprxbR@->j82?QKvtf3B48XL3PY_3p;cx`iGZj?$VNOOJm_^5i(= z@n+tJ$gt&cty6x)XnM1pdpG%eY4=PsjT`?NavvUA?asGy`S(2;jjiHmTV5^mQ9QME zk!_ON!mD>@_3buoF)1l|$JZV#B@n2##{AfopNIZz+?2VgNQYHLzsCNs{G5n_;5{7g zGV=|NxddI&O+SB4fqf|x=W?N23|g-qebbo8UTrwVF5=7Q9p~>cFW`E+H9bFEY3-4Y zx(BZXU*9sH6DXG7QF*H%?&EKP${X1$I~K5akpo_v+MeIX7BC(zJj$^|1-qIPgZs*h}vJwGJ(NO{dUvEC1%_|Z#FKH zXso`*b?J)G2lqUuJ&!pIR_xus_&KX4_vT-ZmouzPdvRmdjO?7*^REVFqaI;wVM7e~Shu7mDLQuk-@sXJ_wkJR!>DKTc7#P)%CagmgPM1f%5 z`Xfi3m6$KtS781%JPX9~KHqFlxSfFZA4X|2!V^=tVE?@2_B8ERs6! zxnHrK^NB5wDjSQoK9P48b(^*}=hs!^Ns3#Tw*NTh5_a#5>GeFrHV3BV4mFS7Hh8YR zo%){J&r(05^y|Ag*T>U;$UeLAv5M`VPh{=!4NuNC{z%U^?)=`&b;(q<>UTjLBXgj; zUdXOr=M>)L%s{@=s*X~T{6txNcXL;m#k5~rpG^_MH#xzCAW;;qq!KI4mgIBs1~J*(ik>T(QAlJ4GB+X6@&XedM6JOPw+6&pqSz zcm)d?Q!$s>lWR*JHdW>H+*)6>-{w(|<)t6;%eXrDT)OpNKen3lSSNbPjlD*@uYH(R z*5hxyA!f(Yd-h7%^O^Sk+4%U~#D`3VVvB?N=OymhBVPWn_CVE#2Vz1CBK|Xc?7SGI zexU5{-^T}E?TXEKm-armJe7~DXwj9q^1fC_jgF_-N1WB#CAV-_o4Y2j?1C+O?;G5_ zI(bRKynP0WV!~XyavG_L-$h=9?BUx{-ni;Q=0>G851%>jU~~#{{C%L+uaW6~_WnI$ z?8c`o&Z;ojo()%$$=r}9Q25A1R8)1DYKQ*q_i8r2PNIjSKiu=G;>nToZ{N`xXp*b1 z7Sh4-W>2l{3AT8*UFG-A*p{g>xJ;7yDORd`+VE{{7syKC@Ew;?L*83c@CDU%aYGQfz#F#%=oBLYLM%j{fU|Ep9tM zv=V=>uzrDica8sV$S<%-4>@!`HGjlj-9c^ zK{m=ceOI+nmr%pf*`Fkq%w>DK@|DMf*kCgj%hE+LejiNp8jsCrnrF5pedO{GZ|Fp(ty& z+k38lNU}33Ny_dm@-^NQ#5^@WzwJZRUgy)D+*u29d~`n_O41i!%%36LBR}0dFkW|l zgxc9J?-@&S=cGQ!*?o#{>aiDpqnloDH80P3ZJ{e>t68ur$E90Vz%@MsxS|wkP@2q+?d)r+j@!r%3 zf$RtS;+|KyP1@}HXXgPAM%$Ox!hg>d-x_4+ms3~um$5ahqv zdCTvG+L!msc}h>XUGGg`178h)KpvPkvK z%0^wcf1G7W>t3}u?-f@)AS`#{z}m(8st<@=cU%~;e$8jDPaOv@Fn3>Uu2j&zyhctd zduv`s>esBxlOG&rX?I_|$HeTT#=Ldq-M&ReGMd-Sf?CU-6{NC;XGSi6EU463#lL&v z!_6r!Rz4N(lkX?5S?79j_43dH>wt8&-lx5VXP&T$r=8<>Xq?XE!n}9Bg{!mY!zX8* zvrDpSI}iVTG`WZ0yyu1Q@upYVYs_;!H~zdNmoo3MTP)u}1Hlu;$IjW$>1V3lFH&%L zzf0^-|H*!Ov-e~NZZ^N2__IwqqHgw;Ztq+Fp;%CeB`doJBPWbh{{+rXZS{DSq zU#|J?ytLru+K)#cuHNx$zTst_r~ces!58E|o3np-8GLM`#>7{bzROE&&fZlvdEUq1 zupK|*_8wlm>v8mtf~>i6+bZ|^KWFyXn3?Qg>Kk9QY3A-d=i!tE;1Eunsn6V&T>B!_Vd+z(|06!ICMx`@L#WVWi6BE z<%>PgCAjII`PWGj!o_K(QyLVh@sIobj|PIQ!vrHoW5M|=Rv_uQYw)Pj?Hv2)yUg=G)0lO!IFImljz||6LPqe zUh_|T!W$g8qmg6n-lx}+J>J@V?6|Tx&TUbd6{hWo)+uFa=x418y|C%Lk+hef`KhEyE>+SCzzu#Oj(Ov4- zkJAayS*{=cePnyy_IpxsF=>qkMsr@D^84U&Ii=9zg2|l1`^Pw27R?Abn)+vlp|#D0 z*7u7Zu4rahwQtkq9J7e7%|c4gL?66oHWXG(%-@@cRhRLR#zhe7b%&%ddY-45V z^n%NRb=MvTT;97e)VxaNaOwuF;^U9{6jpj>xXm!FyHUJHjY(T$;?Kgzy^}@w%(6PqoR*fo zYa)~AdB(b|*5cLi{J+ku4Gg<9O8Ph%Q+WO}w6RKc9G;iX-6HJzxA63H%cIZk#aXM_ zeR!#-F+nKs@~a=uZ4}-tEl-nBzQ|Fjza!u8!q*b#84s5*g^MrvcG%T%9nbAnwl#|u z%u$woeCFBYByOo$h5Vt8ukYHtI5Oz~$IBD<`#AhMc@pK4lA6sKFXV_8h&@{3=A zf@k@p&(RkfjqWRFK8#t#a)YO_q<)6s?)HUC3{2E#lyt6p+4R9~Uqbn`h>W^jhYN3% zUM+Yev%r2bYs>0Q*AE`nXDoZ4XY%0KG}XoW44FSuee7bCXUvpXD40_*nP)p|;C;Vu zJ3M=p*SO4&%r)Lv<+tEq&Xneu*1-K1rOsM!bJwZwSTp%j*I(g&`N^$IPfS<;{`!o} z#qEzAJ{>N*v20)5d~wrn+06&`&P(E3FypS8Cs%&BQlQzV7<<)ad`sgeO4Y2X@L!bi z^75RLOy(DP*$38o%Kn}beLDNhC(Ft+Izj9HGenxa&^-U2LHeks*VD|Z{Soh99eA-; zKI$xwrkv;<8M7M;8aF{bv?3aojdxY+1O1J#WE-9Gxc1b@Fl&TX&x5nx`J&6;>@}=`OO}^M~;I()JYx zMfx1RerHRnx6(J4sMvbLEi1K+X?nF#=KhFguht11+ovtK6;&SSwnOr^f$moKFy_-; zD=&H(eb{?l>vaGFpY^95yE!?S8D7OzTWFoVJ!6do|DS^j4Witq_P;X{Z0hLJUiS6# zBz7&^_J!#_zSpwmN=WRwZgKg=Hk-TG=O!IUV9?mVH|yc8PghF9rk*{b*ljj>?(fc7 z?YSkzJrWz{_$N1SFG<+tzAs(#O^0k*cHfQWYa3>(Px$fFv?h1M1?EGq1w?v-8?7W( zsfcmEyjVDaZAtI(nIDb^y$KBKRkFDg7@hlgqL%!@bFNPU7%yMj<`Z^cY4B@qw-d7u z>@a=yLHN0FAV+TSQ@MQwivt_h&dGmpa@)ep^D(P=Yf>kOr@ZXV-jiUuV$X@({|qxE z%-tIzw;C;)x9} z?%y=f+WXMJN$TafA3Ihn^0xhxxXJKhhfZff+y|*Lozf1DPb}Bw{{GstR@dnLrJBkq zYAZf|FbL;qRApYw@wj=B-mlLe5*AoXGkpE_^_onkW#9qDXVUxJI`_Aq+n`pW_QQO- z=ZYof2ZMhYUYxCWvyS1*=|Zm64a_rAa{|)?^A8rSYW#HBx;nvlvxdgKKJ(X|s!VMC zPfA-SGdNx^`~A#BM8m-J>JGOB2U+~KOB?xDY>)PnychhGA)>dsF?q$$n~&>dAFa^n z=iSVBCw9{-%e3YVZIjOwO%W7b@z4F3n}qXh-sOvzp3~s8a9tVw`><4zs&mSmVL)&0Z0g&VUqWHnuWzczon z?)coSq{{8FrHgh1YbNlf-T1|8E4)6GuXyCfrG<@>G&n9`C!Tvjrm$GfO zlUdj_rP6tFkJrNOG1H&EPg4kt$}tyEV##`0^}E8b-~Ec6@MF`f?n<}5d_FFrBBWy9 z_+>@~i}$0y7HwC4IF|T$U3&9A`^=vQT&toS)t^2&xMGUc`GdC`_ja{#?>R0%Rovln zJCjLK?}f_W3Eko^-dH_7GUu(@CpQl3S|g?Wi7fiM{~3(dos!~il`Ac|e$Om!la@U5 z`#qtpY*mTR6gM=TUGpMbeLIWN<^=~Qs4nESiE&+)Bg1e*esvd%)Pjw7{`_o-dZ8#Y ze|6u54NrI1?3!<3=Ku*W}3{g?7Sc?o@#C!9OaY%~MZ7*& zoaNv)*F^P~l%d~!?a9yf|1mhwc(PRZQ<1eozu@<`#|0Vt8<_gv?M|#w6T5lIB0_NU z-r}j-66}O~xPr1YXtXd={bVv<&iZiQkqx5d?(%GI zQ#(QyzFN|wU7|ZjwB}E$Mfrw5yJ~h^(mhz~xBi|(=RUK~$x~|&YIUysc~EKZ9P2o_ zqSlLx9<&|bmtbD-bC$_|dkM|!UK!4gUn*yBT&=<0S9)y&x5GI`YxSQUDNDXMzT-PR zxrw38EH6RPJC2=a9fN3m@1|OxXWXky^z*Fe=WzUG-+ZUQcgnNg+{Zl&pQyD>s_#iV z&mOabd9ihu!6%oVId`VDecK%ScjmT)7YlkH+~0M=io@^Xv-xXy*O@K%4Hmx=;gVog zKjY{uA;kpSzYn=ryxnd6culN=du*BV-6bcbmU~}2`R|cf&@MSYfAc9UU5sY&ukS=F zY+hJsTIkBSIc9p--{R>>UZn?ZYYOKwZxn2QdyGlsv%c<5b(?<$4tsX#M`$TOcDyfj zc1i@RyG6mez886xtE)D(K7RhWQ@Jrj!2EXC!o@Y5w&8+p5uN^9&zW2j__R5%$f8wW z`O&S(87%g;kIntsHuzdhykNVl{M<)2=|E@3i10p7GtL(OJh-go#oaj@FS7)s^)>BV zJm&zHy5Orhb}3vOCLF9mrNtetEW9D1uh%BL>*dZZH&~?o`0Njf#L}WJ&KM>4Z+o!a&EcBAa>TXN^m={{@9$l0^&y>@o1h<^CKP`MWq1S9Vn zhE$p;F(C@eD#ZXV%^8A@45dO)@^KEc&5Sg4E7L+9|o7DH8VX^RfhiGoa!d?~Q4g0^#RG51x z)PCFPk})-lo%hij_OkmEndb!Tp1k=7KhI9K49Cs&7FDZWo_78uT{K(vTdF{HTu;)3 z?6;fuFdcErihlX+!6E-dYklG8(qcQU7QWn5yQQH#IPPa5@8VU@G9MN%SmImr<0&$-PI5yd38Q@grCs^Cl^N2A?`i!U~RYY5B@3ZtbNAQ!c$3#7}gF?T& zwqQMT#o_(K6N=kJLxN6yv3u$*^`h98(;5Zg&a2ap70$|Am1DrdA@-_sacsgAli;q`^G>%&$sP@K z4?TFP-$T6NO<|TI)6@^Gw_co5-(a%KW?$$`t*CAb{;cw2%EvgeulQQ0DaqvZu50~N z+PE=s0lRYk9I+cKRHi=5czNPmmw0p0`G!*>dd{vd+Lb$wa+$=g*OQyOKkgj!Htq=> zbUjmQE!8V*sJ*E9G)HQt5)|pl+4?D_veSB zIXP2q)ukU}m>Mea)`G3T@>$n`UA0w@L>(DEm|cC@#2_ZH^!M*mCUX`(DddQo6}cvV zsfdqHXks?E>JjY%eZ4K`^%QsaRDXCR6U}8d>xqO`hiXXsIk&R`TJE(wL_8eIT}^iM zYBhb`?D=(8S>yc7zcUP488hbX{&46JSK{gHNetT8KgTR+63WTk+w%0qQK45i^4Mn{ zEZrn?;DJHVrxiT;MyX4+cA4ybFPbv-^wL->yRC(#(jMC9L`y4dUL5f#D$Aae=*{}X zEau(xV^e?C_*`DIcGHBTx0zzUrAnK*{!MYXAEBT+f5)HNl%mVwt74W~l=U9el4Rl5 zIDO|}rSQ#`rCDP4U$0Tye)gt%(9<(P3-8}vljtzX_)b}?BJ*^)IF^|kZ&n|Vc^uF5 zeVXM4k()PvoYZ!I@p{J`ON}L0BKc&RH{7)7@7X09u%73}XHl(O#}H4${|qe-E&J^^ zwr!sgD0=a1v8&6?307^nt!o)IFK#`k?oyYy$=TX9lB1#Ue45f!28XV{a~}(GHWZ1x z{5iw=W|^vL1U62MvJn`GB>q* zuH10{x&<3YY>w2;9*<8frrXOIL}ZnwyuX_|Wy#_D-#)j@=56}4_w{zkCk}lNKIEJ$ zX3YJYSf$)h|8s)sR{e*0A0~1$vdlVXomzkT?af)IUVOW-_FVG9@a>ZY6P=9awCg_? zTDsdkX5!|^LO*NCp3aYF-}5V{M(^VL{?32##8W@sFE=UOpkn#cP-fMI`}S&vMIn!$ zb0^#hzWBCi*1GTa#PUCW{n(iOaN;xjp4ZxU-c-%ty8NHv>d!;VJ#Q*k-LI5=JHzpE zYi}Ljua`S1QKhGnJ3o2L)%wW}2I9**(tDetXv4(a&w((XglGJ^*j-`_D1=CCUL zU3uW*l)$ZJ^Vo8}6rbVu_Nz1Gy0qBne(lk`6AA(Ex{I?H+>O6;f5)073K!<)@2#Ft z)@%JE>IwgRk%sv%c3hb|A?lY!l&0q*=Ou?t}!Q8obrs%8ZSH#(J!uHp0=V5{~yWaH%H*$Hl1#{yn_9WcKPB%hOFUqt8MkeZ4RU_+Oj(? zM#^c@dw2EA6N?%zoP8m$Z!yz|Y2M+lod-^staq2SI^62I&}4Rxi^}eWd>m!%JC8Vq z-slYfTok*|n(uPB)*O)&Y(ak{rm`AU-*V?Yo}3ivaWP8D&Vc)endhy!auNnwujU9` za+}envR2pl{qGY~Z%EfD{(bgqH_P40Pw!W-HK%lmn(bI?8O1j}>+_fMS6Q3(ubq9( zQS6<<8kM;19n#EI`=;3jHt^%$PwTCwfwYbGDngFiPsdcss>H~WO_ov4rly!LxbiX-WNS`Q};Y!^83$lXI-#odT7O`L}nMu;!Lh{y6!BzPi6Di z?u43^DOO%RyL#2ZHM`HTo|j~e^^%ca_o*OzfAEGon{MQ5C#re46>t1`msvmg`}X;J zbZ39H*s-Gc=oh0B`>J~!JHqzb8=erDn_Vm-YFE%wR&~HezPYBzqESOO^VgBxjeLv$ zGZ?rA?XR2RR$;#Nh{4OG#AO;8MPeT>bFRAQ@T6deAg_&N;1x?5CiMlImOee^P|3^o zyMQI+0?%*p-#kA%?o6ERBh<0lz-jitF?;X4&$mIPWaZ_kl7DBE1sA7i|NfJ<)Wz^n)y%_NIo94k z`T3oqf`6{UEcF*hjAN$z@Rao{eqYYx<=R{J-SOu4sm-Yos(X`|gJy^AD}NTs?41$y zJ#jPFq>zapzPx5rK9RrEBbW2l+clyB8q!`fm-IQ6_=L>)x2Wb>qe|J=t_Ir~wHbvW zO3FI33MM&d@xDL9_0A;M?u&)&5!Xwe4?8-tw?wn^bVTU6=gsanek)d!U?Fj0_6`Gs z2fHrx%t(59K2Y(P!Mlem-Zu-pTGyEK@_Oac2e-DkeR#k8*ZG1trCyfRq{OrCStgamb&W~p+Ywg@nN2VMGZc!g^D+N!_klNIa(2zp zLrI~EcXLaQxaump*#I--ey`T%9Fp*Lxn5>*x^3b2W^{ja&(#xbI1gkW@T&%4+P!e$Iw63|9`@w@%1yYP#A87wL_&}%PnB9Md z?&uqdxqaEndszQ7yf^+){&kW%9}~})`^_D_QJLrOvEQ!l&F%|fj>-I2P_5V*^6A24 z?vNfv^R6peTbSlOTv8}zH0{fTi+uibm_&qLMwR->ti09I;B;w@M9zYB2mDLFb7-q8 zFi&KYbzXj{H}S8fv$seD59`-LUW+p&jqN(ikCnLyUT^AtAGuhmp`-KT>Apr~4{P`S zb|yi;+t>5=x2v$PiEGq(%xG}mBkiYW&w{miehokW8BBe$;>FyY!lSdBzf9SobT&ay z?DM^{q_zAjzaJBFP2gIe`0=xeB(KVr>z|Fj9lg}`a?33V;ZsW$l)4}H7<>wzw1eMy zu}T07+ZwZ)J%Cz-I_<>3w658p`KENX zP<~(T*?mr*22W;Rk{X_8dIm;d$2l{nJ*~PnRc@GQ2s_ z=y9j)b@z1(RR`}C9eyX@xyD}HyKMUh%Ub5JpHJ@VnPp39WW;W~>HDlK@~~x$?&c^? zrQ*3dCfwVr^miZjH0{4p+Of+)ciqmI)?N=?)*RD&3<49&WbHmmu2QU2tW5JTI$D(_ z@F$fs?e@L%hGOS+4E*@+|J%+H-t1W)A8fXIubx)heAmm@ymm{dM-^Qv{iI;*^7C*6 z*KW1*%Y`=@-iuC;Jmn?Z@ThLmWW<=r}(w-lD+*tzf*px$jykzjqAP=Grz;XBY*bs+EaVY zuO*qRW83x2=Q_udjwg#vo^8olY`P|WhECT!lfT6%2y;jO7ISdU$a}| zYUYJ`>70KyT;<)l?t$o0ww99(&yN1n30pWZ;PQbauXCKPkNTHOGG7qgAQrPo)r)oa z`}dNL3;Q*MS(vkLa(=QtdT6Q>@2f{IoH99n{COg~!Lh6O;(}wdHopDUxtUq`MX&hW z8Ox43tu&MNzUH#ZQAFg6nsnOJv`=hOryP?n1YWY(7cVnzESLPJk9l_xjkob$ccdp6~G_v4n6XTClY+i!8UPj1-qSF znfv;ElNm3?T;*@)%-&>q*YJ?>r;6iItA5>VbAA;1`blbQ0^hD*M^{%IsMGE@4es%b ziFe_8_wS>&b&%a1w_T;|qUt+uJ3M%pbW+i)q1s)8d52WR?c)(Db8L^zs(jDMVBy&I zlbLPJfy6AviqlA&N9&=#xtjmNGY+G9%c$CjB7~Pb7jhfW!I)Y*HEzy z@ZMYcy~S~R--)7E6E|OsJHAFjK5y&b>b}QoHKeD{xmhl#dT%;ca@&39d2<{imD}ykozYw_@`LY= z*LUkjPj(gkXW%^N)$l%Jvhc+_uTFpLxvS8tDj&1ms<`?@z53U=B0H~yPqI+r(s8tq zi- zO_NG<-oGm2ipaxPU;Sr@}w)$d@^-Q_EA*4>NcwJFmaeigqDOP`qgH%7d8sr;$P^B(_pr@qsr z#o65~#UP^w) z)F12Bgf9wuR^0|ov~~7O!`pSU%GJ8H_cNP^E!KjuW0vrr#`-YSm~vx;FYqS z2|Qh$XV>%+~&nClyTEsjn0u&u?_V%La07Z1%Am0Epk?`x6mQtN&% zJF~fpJ*n)+jIWvzZ!2CF)LFE1S!D0Il(?~Swe0u4ovB~+`x0grOw5syFmhY_pFvG+ z+o_ukt2FowIWnw$H5;eA5q|qj_6Cbw!F^}>sADYeM7vGTpFPAespjtAhbNv&uIyFX zeD|m1*RE|0tM}#GnNDGt@!(tT`j7p*Ov_rW4*Od%Xnt)lt;)OZ#-y=|k=M18r(^F| zi-og97hKMH9`jghFYlrW-4iZFN$uHDbL3w0fhd!h+|W=K^@<%aR-8*;_TKxcu|tCG z=(Yb07T!--TUR`1I@{GSXQxK$>8(qfC2sa^nUq?nu6c}QhS)l_u7pz6dG-0PCH0yb z8PwI!F~rJU5E5v9a<+-7ky?QEOQc{-Sly=v2``R^2cwX}rJ z?8{`ib@^QEe})NB`T7A|%@R+4&5%0i*AVdhJ*)S@hwho5r&X?+&Bp4ncK;0V?RvS{ zzAeuzj2WWa_VW8YWc@bvb-TbJZbt>*o_1xwj|Pz@y0hk2bl3|_lyM!?|@eXtH z>#}q2m~5>xULJbl(Q={YP4};R{4+hb?~;BkB49E3(#!SM$Jc+nyraA=ZAOZ>ZsFSK zJ%&qTuI8x-Tra=o+1=w&HuL&JyJa=^mYWz%3*FzN(XS^s=- z%;PO9)1oA)-OVW%9&}8*wKSgN<&4!@ebq;AyD2Pk5f5+c`+0O@hVuex{RoALvC$uw z&)?IsCGyCx8GK6%KJ0jZoT*8Af9Av}Zbcvhbzm0-Ro>MyMpxI#|xv_40Ij2TBn?==wf5v#}{#k zDfcJelGjpZ2SPbMdL->R%dM#M@bzn@E#=#u-MAFaT-wYNT*BA!n2WziS#96H4-Taa z0_JbOo3JVFy?d`QD(!+t)G;ylG`>C_MdN6RBc34{+o#`i%37ds!KXk$lFO>>o#d() zv#UD4JznTF`E0iT!^6go`y8V_v^nhDDSho#Nn62$BHuU3cO*R1gK8NPLwOrI&#@M2 zbSTFJ@BVBkvO)1{_NSdfd~Vme15TcjYIX7JyVED7&~Zzq#=B3{`}D)*uO2Bf2|oR5 zI8n+{N?hf~#}iwkxqr_(?kaf0lAY4(jPkbz@U#R&nX~!J-clUeVY_59x zSYqnW+xz!OM}<2+yp+_iBX(kOhoHo#Hy$>g-J34lzN};0pZc`jaj$;V*AuQsulsPQ z+@A&jvyy|wnfJC5dAs~cXfYWKCW+q89QK+EZxp1dQf zSB@T@+NvV%oBi&NcIoR=EW&0RoC}(+?KHgX8kW~xS#;cR_34L(U!Kgm7nis8c=PO8 zD)$&d#hlluy|=Qta8ydBwURGfS9?kLbT>_n6CM-057bEA=MbDwqm^zLy*IwOqFd>% zzx3tVk$s5)Pd9ZdTDn?EoS8Bs`0>$W9}GngtdDqUqX*QnNUbU2W zPQ+hB+4g;$ZJt{t?#b;2P#2Ti>;dCq->s-s_pS9djv zDxNvI;pu61|2?mkmi7y+%6WRAJ7(^?`Igq-}zyH|q-K6ezb$ z^{m$XlaxPsazTiDR?0tyN%d!>mrF>OSc|VeaGas2etL)3606XaJx_(_`><=A_FUck zpFwS5$eVm|xg)U)ivEPN>a1*Bs68=n+4*wqwY9vbmAka_)~B6FyRC3JpmcVa>ZQ6H z)|vhWk;1XI5)ZVbTR$Ax-oJwF?lsH0k6Zr4T+LG8Xz7#VUD=padXH!Ay9+iwy&pmY z?%7M;IJVc|1&3(zAMVae>e5e2>|$~wFT5_FEPi>%g82)V9O2Eq>kvM7M-Fek8Uusc zB^#F43a?G)iFbT!tn*c~kmcyPcwz>7waibm`8^#$A}bu`mOasR3=Df=dSph3pr%PB z{~SYSkAKJg@;~Q&(@FWyU_ARj!^x&a+-y^=kF_4y8zuBH(Vlf4r@;&!C&L-NPbzdo z9=f*Y2`u`Q@0nENv#v%%<=J|*XS^4VF1Hs*Shgr+|IEBBCc$9u3cE9@7dU@eoxCeD zjj4W}_<|Q{2d}@JW;IQ$sPp6k53hh74=p}RaR|3WmdY8N;MS6AIq)&Sa=oCPjG>k6 zwE05c`)^OkQEu2T;lL0W{iIrN`W}ZDH#iRdaGdI#@3>BMMq|1c)77hU|0ISmbgaEKMssEF3?)1{E@Rrw{J!|eX^2WC><~x3T3yb~K=jt5+6E24CdsOmW zu=MJ(o~U&pystq^J~!Ak>~ze$THZmG#u*fD$dd8MyD zQtjE#Hvc%)C-&~vp6E7XkC!$7rt7B}uJGKvMvCvjp7jzh8t<%i_P?hYAik@7bDFAm z#Qy3w_MVC6XU(4GUYl4Vvzo1FMakZn=wmBb7JvLvd2|cMX_46SNC~vIGI#^?z|;~{@NFlE6=C5EB3~`<#VWv zpDq-)%GAO*KqBd#FZ(i&*|is!R1`lw&?~$2S^<}b(%Ew#Ht@VI)vri<9GSAySFP(h z@9odm95j-5e7;hBcHevUWA4Y=?#9f}VqCP9<3aM0cLye`R7A=1My|ZjaXT&Dp!@p; z&a-P|*F4zt_=ZEB5AWe$T2=)w_VT|!Fqf_6!DGfF2PRzXJ@K{HbWfb$#$C(48ws!L zWWG`T=ExnT9{+W_js#~pZrsT?Ycta(FOAs;_&C=dWXnDiG?%SVX47|nxBm=z0%4Os zzi)E-7J6^{@sHi66)SE}QEy?A!YE^Lyr5iUm>5r>w(m z4n5R6TeZT+)L@;xY)#USg7& zzh>eEg;%RTz7zGh_GNXRaOMV)Jv?5g8LvG39H!&20X2#lGQ$+6a_i*(&Oclv3 zyzpw`i)UVQTi?cLZ*&mo&iye%X^)4w%{2oRM%VDJk8NM{S4sJ-4ebc>pZDX0!J5tU z{vJER7dQKAq5SL$<&&SM+;CVhjk&}2)RO4%RUfMNw68s@TC%@VLgQ->xRi zee>-RkECOrcMn{<;dZt0)Wq~sZibgj?^!KcbT2Jxkweu)gA=WrPIq^$|68yid7rT3 zxs}Y@dsGtI`Bo%^vU)H+DGX$_`|IWZxY7_F1w3Ix7^RVkbCztpX;IU$@5}o zsH@~9=lLjcuKUUI^T0F_&eV0=_rL5(x^|cS_!2TOQOR=s*#aOZGfXxxG? z&Nc73RM}T`SIoCMq2JJYVexYjsTJZ57UB1M80V{)uPxiNW96OFTPs4u7h8N;be+?} zzu;oPmUnEIjOG=zIJZ1{VU*LsoAU7VHkQRH6Eof(SWx4WvFC2V&fQE)vzN7ATqC;Z zDaY%%S#cW;P0k81y%Ua@dy?TH$HKD6i6)|V+%#%-8Fo!vrQW&qXSV4or)`Wecc%Y5 zdF$Z1`-|&-c1<$LSQI}?_KZUwhm+PWgG0(8Q=;a4d9C8`i$lHj=+Z0u6JGOjd2Z3t zw|?_p#Z7_5KCf^s)789t!fR~8Pj9_l>*LhY$KrA+RpM1fPQiBJ?1!ndMC7k+5EL~x z6rWeQV@kJUf(IIxbVWRUN(8EhT(48bJr`iL`zxcEVVci{opU#uj46v z(wWi=?R&y^a@F;(xogF$|KMcf_t`&Vd?OU6{*rpnW!j>z@KEDiTZ>liyYjYGUGF<2 zUJ7LDZ!de)^wlH&=4K(mmCFwO`h8^I(YXaL?@2dTIn4i*5@5QR?@ke~qJ?NiU191? z`R3EV&iSY~Ct4req42_D-JGZQro27)E^K1q#*}I~Jw=wg+1;Oy3FP~JiV$IVf1O!* zVd!GRM?A~Kx7r%G&G`87n2YVvD%sqEXQ5M?Bl`Fvm#Q7wb7$`2dPcc?_o$@H5g)wc zmKbF(y??Wn--Lh7Oqtfdd}Vu7vd$&^EuE6J`f`=WpO^`&v*eUsoDAA2v?I-b>6Uv{ z8)vD9+O_h&y{&jjdxPWU?qxrASzD;wZc%S87Utg?V{z(;#_<^s%6LTg_OO=;mg&r} zKfA5v?T(|G-Rsvm8a%j@*151(Y9iC~J|3;cOB^p|Z$58m5g=)iz{Tvu@MT5f4iCQY zDGz_1w3OisU(C0?<<{G8Cl@3y@3O2uEztYqnB}zjGS3(OGK@?8c%(MGqd-Mq@6=y1 zrDk{JjvwG*>#}a=`Cxx~j@?>;vx)~L{>@zb#Vjvz_BxJ#MN7i|wf<*VmLGMy>O<^| zWa~|@&m~J;jOg+4O%pt9D{nZnWv@y6X4TnqpFNp)zvgT6qdmvGO5b_zT5aRl5>aY>G-hshaNvfsc~v5kYpU|(TO9-TSv+XGdD-dq zguiRqx?<;BM!(Gxo$TIoXy&Bl6ZrTHSeG?t)b2g}_Q>3*l*>G&U&}Xhwl}c+G%Ye$ zKU-y^^~gw{S9rtWgU2>~(k-(rd%Go2%|CTvaDH}cT@s&J_-6LQ4Y#fK))izj@$fpm z4?eS*?bYdY7rtGGM0hgtUBo^mtZd>iJ9gyNj8zU|OA_y$Nz%|+n7Nd1esRDB!7_mv zc_CigzcH@~^$Or-_`aUUQNh0<^}%{^W)-t0#dPk?tQxDPHs3QTogU@5O}eDNZo}-^ z3zRDTdZdKJJy~*OE=b3no1=7Yxx|DTg{}k3me*S(Tz1O8V|K3HI`U@6?u{+t5sH0Z zI8TbKTxZA~BFh}Z81YzE%5+^{FHmorQWS;)n{ApUvo`D>PLy`B$EXjZ&kLRtNYL; z?)W5U*N?MKjJ&U+&n~n+lG59z9T2&0pXxKI zWMO&0HDkle4+>&3^;PRsrho4I((L`?sMFS4yzSb2A1AVwzWccBh~C^s=YBEB^savR z+AQqsm9Mo=3N$`Dyfwf6o>3=iN8t1lfzr#}vE4JCzGmuJmG5+qop+)}Va36lw`U(p z*`39@MI!cR(VhtbEA!)zo~^q3>qm0%jbg_QaRxj|dVF3o$62_(uP(^HG}S3s(6@5l zwC!`MQ#No`F898?cV6njNa^IuGLyAe%|5u!B{E`j>jAzAuf4YY`rhOrrMR!4Jh1W6 zv;5$to-&+Gt4`%U7Cp2iTjAx3^(o0A#mmp^DCA77Exx z3=AztueR}pICCf7s?H5(a28LV+I`|mky+G|1179~yD!9jT=}E^?*k#RMa2vCl$03M zdvgsg*tGp;2v_L%J44BQcY1tY0B>1-&#HSO!NproMsM-Sy0Ca2^XJ*BZmN&w%-qVZ zdgta85x!Z4cet+~_)|S$LsWQ`MbW2F6Msj9-hR$6Yayri^Lq7(&qrkoKR4XjDqe8!V@KeUImXd% z3Jb0|U$H6L!=34$A89W)SMT?sgu>(NpZ9Fp@JIIZ9!7`#B@h3mUR&4@wNdKoliu2t zBMG9*{(BaQ>ztDH1DwF5p_#{`~ZnAY)Pd2+)CA0fKg*sl9M}8c4QQ$J@oB7Oo zM3yqEd+xWUb~|}r-ka3J zdd2fCUxwIEUmMXQ0cENC`PYO6b9$+#CF+Ju$dFg~+|Jbc#m{2P4|{$5my<` zb3uBQ9l!gYCpL#}Z7QyJk68Tv>;686GnQ{n6yyT^>tkK*!n~zz$7hja2PFln?ZRff zn#|O3o^9?-$&jcmVYizWbSOuEUVm-_gZ>K54?(ZxGP`mFAJ&vMHmetIxjkJ`i6dq2 z$M4Vbm^>X9UHyK063>EdP3;{S#XTq3^W4vQc{E0v9AAAvZ)?)>;}PQfYNQ?@le{wgT)rN3xY5Vnu<@R#%zUe_*Z3nD9jotsc`wCe%Cx&EN#JX(j-hI9^{o^^ z#)PugoVhV?7o8KIZK&7l;I_VUw~aTiw$6J|yAD6^GX_GR(l4K$GATm(fM}_}rJp|8 z3=1ar*LTm-a$O~|@_beC1W8$5>9-vnicYy7?$2S;lIlH>T0Rz zZ)P~LHDuX?)88_s<)c`O4#k6qVGrat6pk5tMK^RQtg}?$z9fI@AJ>+F=(h>uHt=IG4rdu;laBnO6|&? zS}!tNS;2a-p^!y4@XM)mbCYXU9DGXu0 zeCy3ICM$gwgI7CW>g_WQ@tgFuL#xQyUEYGtcfs!`%V&OMVXdE=s$ea1H{dxpN6NkV zJ!=_Qy?cXKZp{_@l$@Vco3iKb?f2eR#|0Lw68wEOJmH>7#et(oq-EEa9?&$oU{KzA zi!aMuXx)}-`Q__Ewlw=bEWGjVRZLAU&&3o- zIJcSh&rP>BX?a`Nw0P2W(RBiqb1zr&FnxRd*5EGVB4**EE0}w$9wnCu?OboXvUzqF zf0mwTrj>Zj;m=L)EAH-!tNUCW61cQ{$x-1;O@6#v&qo}daAm*zpBZ*)6U2X77qT!K zW?qgjIK{nDLI3vl%?h{nDAxSzWBX%KWqbMNM81iEL1IqdyILX-AAGUblDoz1eOz0B zy-DWZ^hSMcw(NCxOge5Z|M2OqyG#RvyM#;4nE9K=*f~T?DOqu$P@>cWZCY80s;3JTejTfmo&Q? zkhA1=&99SJ9CmYG`p93iSucX%PO25!7`$ZpLQ^2z4`HS|2>vPj7GlicK!;-GOZHLOrQnU;Rm!TOB{ zgHO!!v`iVqQ!lo)rMA00;yM=~U4Bnxg+Qpp2mU!r_I9!H>T~5nz7|u((SZ$yN+))+U|LxZ<+gUhb{)`?{hpv z%`Hl-Dh*ZgU*0V}mEgOg%rq|lW=ETfMQ)Y)?>Q&7>sXZ>7T2<6$awPp-kqcselA)5 z*9=@5ceVYry{8f+Cz79CcxPfD!?ogT9=E2Kyq~f&&|g}*U*@MluGY3S?oZzw$WX7? z^Dg)~+n0^%`8${uf@7lGg?MGs-st8r9I0P&dycHoFWriFk9D>5AHK7A@5pp+UyS!^ zgSJIqKlv3c+_!$Z`{jG=4-A~{-L5*%pX7JZe|IOhDU0k(XIT|H_kR+KGnpUCX1+_B zE6U$lagV{Zf%AfR@zbMg4t%qje|yi%pe4@x7Ps9J`2KP6tCZZN1E1Eq|2|?hb657A z9YKjVYyY z9S(f9=d@-|!aR3(35Au)-`DQ@+Us;Ndhz?#BR{mPo{68iAzLdxH)dhnyMNy#PL5}+ zJWx~kacY)CuAb-Ge7_LiQ$-Jxj+lz=^k5OJ)vvc?Rhsy|Cgs$Qh70$LeN#6!l{n4F zb)}4X!b&ri&6A%Cp1NdwXHJH~>^t|rJ(!hsEPEOwC1dI{-{vvl;y8A z3@7fYE=!vF%=Sp2{Ibup17%|!*VY)b+x=%~D7h!%RMIOoKXUoXuU-#azg@W&&?D^9 zF1LBYb?4-}2XZYWT7Sr|kzS&_?e9zhulgS`>%C)oH$1X^e2_I&uT$rMqF1Bfb*VoO z9W=h}skGaD;KG-OjBoQJWLzWx2~Eb7e-iBOkb+4XF^egXMS_AeV1o!=}@GVbhF5WciE`0Z;c5hk@2$Cf-> zqccBTLb@bDS8kqvp^H*))>j!phwi|I59)ISoA*~mGq#@I6Y!?^bbpM}wA-Sya`x+O zb`rSjw)ApXcR4lNe`cK5&ovxJ6)I)@Do+JAP#zTMLQ(CU;z|D;CAb@vSScsj)Di_Cefl(CTcYpd`?IgNV@Ufh#y z6L83QA}o~^H*t+tP+JGn(zflhd{pC@&*t3Qpu0uZAkJ^fd!9AS?)&V!KR3-%>v|>A z|M-quPE$#o`P7aL((CwS@bUfwV-$w2wd zcbn`_g%fMnUb{Z;%X@LY7V}8&z?EyZ@0xSerJ(BJpWKWNw~aT|S2A3(=jPK+6;*fq zloq&JWB>Epy#c2EnKyhky%L-9amBff>nmP}R_5#Pm?**??ZD9Y>$Sv!hhofKJ4?S$ zn|P=s!_R%;iz7?d26Dx{zsK6VEF{2mpZ<K z5#EwV8WLe@KN1V}PT>{FvK<4))g+!4Hqn3f8EXV%>>@d zErw18n+mvI@VL&K)DYZU^P0Wpg6d_>!;OI*svI_XF5=1!(odd0pQG**K$2KwH4T(F%cJ#I&ye{x$o8V zG^0-otaYyLOW!9_djDn54N>WfW$F8`InI`OU6~v)@7=^DsZ4WZtgroNh)HPUI(SH} z?b+$DW#KaQXV_9y_D(8+#7HrYIvj3hSXUqdp=T{%6Cgokd zl(JI0h;?niVcCVZbA12tm3qXg&18SgQoW^NOT~=Y>-x6)&Mr(?xqxk>#etlriz>{S zg|l=;OhZ1&WxMGJF`RQvJuai++rDr4Ip&$XGI!>m<1(9^{rvNXhYoYny+h~s6;1SX zmC`pdPIY|TXE1Tk4N(iusrpUqkIx?~a?pFT-c6P{LcZzkiOn5T1LM?}>8iMUy>8rj zW|gc}>cayE^75@$JR$PxZU*L(VClUl%{&~nx8x4#CE59)x7z2iI z?rm!R-n1#)Imuc!g{4GOVs7-sPQixG>&C^J=Tny~DmFYR+L85Vx|$}#>(_6NaHLe! zoN?4zHl6YFY==qv)B>(9c=h;2$Lo|G(*i{1&5@XrBgVMIu4vMIULSE!jY1yNiRnc6&X8~Uu@2i_@E_H{tSHCm7>(ZN{ zsHi3+64U5gJ;ya+>Z+3ZJH@M|w(v0Tj|g2GTcrH=z|_;spY~TI=RN&+00Uj`!r7?Gh=r7)rZCt zVlP~{JhAYa7t`tkeVhWvq{O=(%?Mm#bk9IlB2>yO;(mcE*Ao{Foxe|+FGn4IP?oK~ ztfTDlv`vejE>*5OpO&#?%3PHT$%bqXf_-=0s7PoxX0c9Y6uY_UNcB3+Lz7-_kUmChJmx_p2 z7p3xuySjg}UvQvxwNv=zuam7_mF+UWTrn%TIZ-Z{+liZXx5%B>qUWTVtwp)^wwa22 zV2HEwG2`HMj;eakq>?2y?RDCQy9*yWF9`Yju}RBE`^1}@o7dctXEmGLfBw_qbxqCL zhfLmooN#li$;35|oZQnUi4%vc}u$`u_Ay`;M9#X+Wq+&4nYG8#n&UKc{l)WX7la$L|R|vik9-qHwW7rQYeF1m`)I8*Jo0zUunO@KZ*r6UNowc#Cl=@A9`X*J{CMu9Nw&&i)m?cOEwPIa?C6RuiH)23 z>twjXq=mf)IUlY_4tl#bCH0aYvt-b{!;OYPdxT0OUW@f{>oYxOjml!VJIncuqi1M> zOXYD5=N-GpH08P?AN0k?FsotR-H-cx?`X5=&0ZWhV+zBiMa`|>1zy`)74A3s z!9Do~pHSp-_t<58{G$I%4cP9mvBYO2?vPb@s}?g-5(alQ%qA_~ZDAwMtztjwCG5&^T4xRoH!#aqq)a=Lv%D_xMDW z`VKqpduFik!K&cgNoTG%oyyoNd|ax}VRbdTqUhpJ>hWt>zP4J`%jZR0pTPIr_)kya zH3hEy(-qT>y|}#Bu5*Go3mY4iUU`HfHkn z>_y7&_2%wlyU*R%wB@#(<7~%QH|}U}vOKKvHo(fSyvuN|+xdN!2Wo6>>^~YvEL_OI z`6BJg4xYs!1pF1>$Eren)?d&c*2JwY9gUA+%EJ?$lhPq~zR`my6HlS`)HJ#|5b zp2ts*dam61_V}DF?Csh2A|k70P0yJzxtmPfa-&GNX>Vfh(yaH{i^YVmbsPE}E}j2Q zNyxYL%1<80y}KDJ|))dhT8 z8n(}qVeeG^`q$ydLDMB`JZ)4Tn*^F_`)Y5`bFh9V?ydA=Mo@EGp#9{E9>dkilLB8q zwmvKM{hHB>L(Q#se;b*o<}Gy<^4p{kBGtWV)?P~?+m*Xo+q`%yy?n3wAG7(Feb08U z`nf1)m%e@H8xKyNmUTgPXX87SJ9>_D=j(auF8RftbFbi0Z1?KiYc8D37w4FZiUP=z>mFL}mf6s&5wWcp$^{3A~;?>g3-fACX`DGQWrN{l+BTlkY z8zQ&wNxprearf#u>?OJ;J7-s)JbQXgqRE2iRtk>sGe4`xJPtc0bj5<3 zA@=}N?k6AXr47~`v)d;3s4&g<_%`eNn%yZ44H;|Z#%-Fp#(~%Ggw4Y15{^rrojjEzY2=$PE6sg`+#4$2El&OOVX>%yLY(dqz;%;AQudVW#A4`zI%tG#;9k&ApzpBWK=&;7aBctHQ8O^-0MdjT>goU3|?c?L%&z;A^E8DUtQR z54MC{7hcX#&nMB~nsWR7@ik#bW4q2N_wH`kDV6$BiaUGT@+${ZpG^I2XT(&T@Wf!s zf(F_96HYl@3_P!ULgCfI4SSD^Ds<*uv|r4~cWv$UKEqZPuZiE!IdC>Q?=?#BO%^yb zPv+5iLr%9j(w2oUG_Kq%bn7acAi==&-K0LE@DdkK`DgRiMZb>x5%0BNPcmU&mi=W> zMW$nyS zI@?ixxX^=3Xjb&y!zs6}DqMJ#>LVMue>s=QyIAktzrLF++xcMg-oFozvKRh5eyUXB zDtpcEX2!#tXMD0)%gA(OS^qWNz{{6>CM#}a2YPON_94L`#Oj%h`+MoI z^2qF6RY?s8nnK;XXRl@`mnq`bUpn(R&g5&HMfiX17Uk%;}$} z9C|%Per<8b>PF`^)w0Q|f_bxrKm2EiT$`%7R$aEb=gZXxNyWtty$+5QeM+he*;}3^ z@MP&tx*l14h;c!NdIirktFGnyS&n$9?P6X1>%g5|tJI&A)g%fi_)kpfpRr0)Mc(Zi zf92{86&?GT%{x0CmS$FX{cOBZ7&38r;s?uxZLJTj(|#ZDVYgD)aNjSJgKOpX{EBP{BeWT9bHZYOK+X1dw^Nl>T9_gTl@^QkVs=ad^2!m%s=xyfag4z^{Q zt4bBM4F5G=bGx9=l=)Qe&J`2kj3)nM4sTao&pVaW?##7p#mNTc6OL9g(^tOptu5+| zo2>Yb^OS9P1e-;}Me$}grp}-Xv*Y|{D(mE0?KOCKEbh=;&3ONc{zQ}Hedqoj;TP@I zK6YLIfYSw2)x8*eOt*w^Ho-=jT@`I&oYo3R8cxAqR zc`efWi)}gw;(qfXh%JMIJ9(3m{7qRfZe@$4uLMpO zZ(r2+HCrcUR`;$N4({(AnOaT@%k`ggxG;5wd_3wL@G)P7``N~6z2D|`Z>lSD3~X3k zUjBWy*#|{|+?6e-TrEp4cK3U=C-%H^spNYyORjLOMC@7f+8u8;cCE;Ly-p_3MfJO1 z&S71($DnAVL`yDEKBHhUE;%K7c&)HnDonBs|&os@051MF#Ju7I0>EeF)>xb3}RO1J&(YKMSQV-=Mvi|3rv5XUKx>TRtmHa{J)x*LR08 zP&BN==AWKRkh=(bQn+3g!|pj}3UB&0#P@wX5)ryw@8iBrU5(-&=2-^4Tf6pt&d-x& zLAxf!$e&axL5Z6sz1uz`O4ITXB`2Qoetyn_4%;$raDKTvsS{wcuKZ z^m560`P09ye{6P8;_!kZnO7Fgv$botcYj{ftA9Y|$bGA`myCt1H~Xba?g@11a6co% zd##^u`!so$Mg_gS_f)v`O*Bem`sOUY#XWJQ)`2Vb$K^h6)b{=Kdb9W?eUI>;2Xh{o zxc&Qk(oD{4?*aQe3mdXHf9JVcpETW{zk{j4JBA_e&$^hsCLF!GZYfF(RTh_TUpcZX znPCEVRCxnS&B;eKlFMr^n)>%9I$V9eXow!U*@+%wk$GnA{K~tpO#DRGe|fMXk-JiUy2zYR z(Mvt^l5+QXDa+^0+1x0`WTGd&elq*A3)i_V^mtfVn6~q-*>aFM@R3mQwkNtxV)q>$ zJy@io%GP%7z|Qlj3q9C7FAD^(^4*{IwR?fXvR8JskCRIonw*N5cHBI$?*3GkH%IT! zTvZ~+{QNmb2%mRy-HQVP)qf2zT=7xPcwbI|Kv%_=KDZ-Yzpt25!s+>h^c5}K27 ztb7}{ex?sQXYTH&N7I@QAF~Q+vY+^9od{=uqqBt0imRozJ1!Ug@nLXj{oF2a|mFh>ZsQoB{6kvMzxRXqyz5~TDo3VhQ3R5I`?f-y=4%qS8dMG z&2qC#eay1G=AJ*t`^DLB%?!;-ex?3{y@6ICJHMWfShDhN#I2s>3jHHzQ;L_IS|rb$ zRrhh{n#AlUhhAL?$-6Jn)#UXqr?%wz|04`)f(*`G~?aG3Ge@k#-zNsBv~P>>!lgUqb8O4{)hUL)ygTK z)Mx+OeDh!T%}za@C*PE-bGu*oAG%qxZEw%b&Bcw;TXX`qxO1Fr+q(E-cP7W}3+MWk zrQO#nf6;b!Ts7-Wa`Oi7lga&UR@2&}CMTyff5^FZVWLPVXX2(S7bSjZZgNdKc{<)@ zJ&(tiZ-2Th)c0&$&*M4ulDI*puEZMIyPZ$m%&HbWYY+Am)eHE!GFwc<>XM+2^&*#m zD=Bj}?k+exHK6Ol)jU?;O6hwSZWpd&b1~nd@jT$+fxyGIj!dEXP9e1u+|Ktn-dT91 z+kL55a@~P;W49F7>cH@hm)o>o?6~~qe#Ya4FLNed)bh1_(d)?h;?l~Csuy1!EjXe!F^FrS zSx)8`tF%ALlU?_AKJ40XX-oJa&D}0rjIN(pc6W`*og6QL+XbJ^N?NWdT$sG*NkOUl z3GA9_18~K4pL2ilCvWu>ce@LBX{+l z2`+O=%T9aU!h0;|=M$%-!;9ASioSi=_wr9ImzI&~hm8xZgcm3F*RRPijg`-r3<*Be|$7Gtz5MhNnc;wpKfl@&^vN7muj4t^FPOpr-S|is_Rt zd+dvDPWp6o`i-ehtHh!OF08zkwx`d#Xz{`b6_X;BZ@CNqI-2x$=lcfg`3k5`JJWnA zX}Rv6(2rSj*iP=5)jWNps$h|x!5_i67q@RbTxhmSVZV4E|G~A~z8l@1F0^mwKc^K@njHJ$=95-g&IxVH_-JdgGdYQWG_Al{0&c0HnPgbnmyCcU;P^Us@t|&*HtEQ{VAhi|dy-6|)piTd%DflBq4Z@o49!modktIES;nYuyx(nVBz> zG`V87aHf>sqQ#emx0i*iROA)@VdD6UcdhT6PS(;z_gkV5|0tANfBeD9C5AgXLeq9G ze=YRQ{E3LBx!1Xk2X?tU=JA}v|Dw%5*|$%ozpZbr(zP4c*4*6g|4St=`a$cmcon;Z zpj%H|+A5rrGeUc<&UKlY{7UUT=Q7R(@{D>+LU_ zcKq0!+QYGd=X2A3I#m{X9$a$a$862~DfX)Cl~hWmr)!#ax1HVGaJA^(XSZ3d!lKrJ zCvL@aC3uz{ipnzi*2kzYtDwN_sNov5tO=fd!iSwEUhA!NEO6BT0*Zm8ZEZ(|HRq2CfySUP9A-zuVjtUM^iyzAAMr zH>TBar$kJQ9w!?UP%K_Xld1Hczh*%$T?GXqH@QVAy@`gU6FRC*~)+xXtS=UocPku9I1G z&c~9LFXz@?686q++P!6)s&A)kO>a?-ui@Hl-^?6lES7v5XmWJpZ_Y}yTOY1ErEM&+ z_S(8oWpU1}tc<%;zp2d@no+XLc5}d?jthqZ<$UjFW~}9Gd-2?L$FgI--^^0AwKcuO zu5NRgm^nT5tY)N_TAh&|CgwaMW2#@tg!CI_cn_}-J7d;6-8?{62i z>C9CVVsE&X3TG8x_;bT$R^GPbn=X96xn7-Dh|^%Qkethehc-#p(~O+E#UwZBCOuse zGU2q{Qqd{bl=M1p8*Efjw4D>};V#X)?e$qruUsbgrHwK|TbBN35RP}`SaWlaR-jGL zLqm=4>L->6`|inAa^EPQoP&MxcpC-t8z{QV_o zhUVn{z;DO4{p0*&Y`e*3qtw$x(Ta0NYicHQwS7`*i`Upz$1+RjwaL7j;wB~k86I3J zS`9hCw&~%fJ^C9ve`u+HnfTuI zd*XL_mX9AZF8a!}GX>P}m>yco_Uc{6%2l((yS_~LZTeIG=vy|&WmnC9sB;9I>OJr4 zuaSS{dz#r6g-O1O?Y>huM&fgXTh}v*^# zN-5kI)=%#F_DAHQot+qY{&5%Q zZWEP=hqe2eI@i_oZu@p_r@)Ng)qzo_`bw)R>@M}Bsr z?TUy(+fujNW^Z?{?Mjb$9=+J7%P4V0bD~_wt%*F}<}+)2IlA(7ip9U1EX$QI?3sL8 zN%}8uowz)kS=&6RQ)2r#C$yQ(e3HMLb<_T@Tm}BG-Z(#)xFX}-u|G+#wOubpr8bM? z&V+wDuXD$Ks>%X%lj$c!_vb~ju3BG#>!9(zkFztk~KV6V%}ttS@TU#2W~c!e+TdMW9e-bD|$ ze%P`4=GsrT=`NaX@3;GuJ?c>EymH8XrTwcP$9mM?3O>#4jN!KMeDKZvwoTZS^8U@q zfA;SZuKZ=gIaMTg@3e>dPR>t)y*FyVnGiJPvi}xkFT*=FreBgns+E^~a6Ksa(Ehfp z*`Gf*Zh5R|Dt}Sz&s>qh?V9Md z&!j%pMa}$k*6eTXTQ(e>J@ruQbHT&?yz4Wka-X<*S?0M%Osd=cV~N2bp2i{eS-ch> ze>eo&UYoJRSWC@f(ykk~GFMA)6BW4iRBCZm?u)H`F49s`3cG&^UTc}ib31g;3FEi_ z84j%6e%d)HOHujZ$}{ zaqz=;{T@r-^0m!QwJeKPdt=V2aY)Kj>f*AODyD)ND{g%DPxUfAnOsY!?Zs zOnBseVAiVXJnc$_2DwRB7JJ=XG_^)Epm_?5s zf4I2rSkAS5yc@T4WFLz(*jRLDxtZDL=AIv}Ej}?Xm$jYzVm;-)XVEXGs9ln(0!Kfz z962=8+4GQ4ftBCOP1`;$j_5uq=h2+(8rRP?yD{0#zsoD?oRbNzJjL+~8exd9ivc7SRLVvyG{zo3Jl~Sb(Cwk>3q_+mZD#A4RvCr>vimpFIIzi67sGvT7k!>3G3 z-wHf&s$=Sj%5?ua>+HQM4fjcZCcPBiaKFuvb?GtFMXMK93r7pPnm@ST)cWkSQb_dW z7deWX6}PXjGn}_MUa-EAQ_Jki9d}x|lva6S6Z$;26`xdn<<@QFciy8u%^E*$uxieU(TNG>?O0G7P?nlHhn(1-LWWZ*42!Ay@#)9f9u#I_@JMqLcvpR z`j?ccFL`&&Ql83rJ5!(~|Au4k=A`&!A+w881&SXgCn-gRFD{AOf1u+&tKCJ0v(KJ= z{Sm`8+xE3iNJu- zCGNFtm7DL@bywB@qE=?$)k3A5ZJM(@KV5jbd@8TltgC;dK6q@o(PNdT6!q}H@@>~Q zcdlm<)4MU_KSPU2@jj+q`vlkddp8NGwk|7nZ#H92FGzwEqX zKf^yKJEd7dF8At&9lzpw^GcJ{$*!*G2Ct}3CJ%hKM84qU`xeeq8F%SdDs$*9treHn z+J#u`-(2ZX|GMR!nAm9!f0M=aEUhuyen>Aadhk0X!k=@h&ei@)Qit9I+$pfzA=fVa zO?aBhc9HTB^HUeUbDH>PcP{kL^fdW(v|@%+o=Tg`H`8jNIf~2AHdUp~i2frk(4&opwig z@&o=Yzx3)^_KEB)UOy>I&~>xNm$UqvQl@?DYIgE1GfzKx)`lbB@P)^^iL!zEmXkO`gf;b2TzzhwrVH zdC9_Jm*d~cIxM;Pt?InTkvf+X-+ulm?tan#v#+>dqO@7gUGomZNtdcE9~vFHbFW2Q z-AaAS(H6Og%T}iYmmFIwKf8UK$;*q9*^LJt^e(w@{LpXR?EL2Rc;%vbZRQp&3X|g# z6*)bor$19XQPe3gaqom1v;60~N+r9UQd@B4MK-@ZPrhc|5!29z;^s2arccTj3F%Et zo5#HNh1tJjtJl=aPVuicoyrn4<(G=oQO&>&s%zRe_V)7Y-BQq8E!L)M$9B0?$8!Bb zcb>my>NhvvY7MBaO7<)(Dd1YZ#c;tTg^LC^9ovm7TrXR;yDa23X}hMfXHoFCR5KHm zvyKKgGJBFP`+lQw{8YcYwu_k+|P*X&e z(E5vx8)|xAH`&gZ^g6jl{edU8p zJHJp(e{m?quTRL!%KJm{dZm;rVwckSywauXb9^q`lUeGhBLAs4@|xvQU1ulZbA7xV zS4__BIAW4<|DcM^GgN7waGCj#+k#S?%KbK_^eH@9xhfO{AzdG_gS~fWO(R81}ey`2WKh9)pKbX13K1uPLeskEd zO>?w97%oc6)@^kaO?q3)8UIpaSH06Ei`5%HSSMdyoOLAWP-VK)#cP_iQ*MdvXBN6w zDWtIFRjT)1!$&^5t*lHS>+r;YQJGIF<6yYR-Aw02lLR8p~PKWQiJ za;nbHbMADJKc+^<^pkHN))L&9vtg&Dz`2|xC!xs~mi`vYja{6X<7DFQDvO6S2tUzFOPHdu9d6(V91oZ^;OsqbH`;**j&?lQ(%Y6D~?8 zx%e}wof6nEFOz*y-x<{{dp3Ukc2Dlq*VeYbQ$==tP}5lOhIh-6pEvJa)}Qj?RdwX9 z7p^ZiExRf!`dXJq^RngrjgRByK1O`z?Ao|~;_WXsCZ^T`dw<=SdvKwWh~xSk_mq`} z+WSsjYujeDN@!8XgE+rBv*#sGnqH`LU$gh^o0eKUBPZg_x{C`JDy^Yb6E^+nE zo2sOlmLKe9pEl8TTO%tg`Xe}fvfG5aKdj<3W{D`)&apC*TKFlfS0HMt^giB8vR9d! z9&!k+`6DK-RcSVVQH5P=zF?H`z7o5$WjY#d7awe#E|!;=l&)su=38UW`Yp8eOLlyU z<{GtQ-%g#}s3$RViMWQF>oLuOhrw=#ofM5$Oxw)T*`YisUrNYHRvR9TgGn?RhafxuWUGg@-q`&K0cm z=r-{^;w#;Q$DLu5w%}XntEqM_X;@Bk7}}muojYdUok;VjqX; zJi`ZG_mj3fDc+QmEnFiop`_JaMk-xd?k0!wF^SLLT2%DY)*Owt;Fc>1db%iCv(50S z^pB9D6Z%a#mybqzY`;@JO6*!bv>)W|**-5a(=Oodg`-bQ%3GoY7d=_~(4|Y9Ug5n6ip1*PZ5tgDy|XI6ER^;$=P+i6Tb1(UJk91deU6_s9A5}C*#y(^MVh( z_~0Mme{iw!{yn~rH|d{oRJrZs_(NJ_UbMKujHiNpEuA74iuE~sqMo=&JxDh#S$fde zdDc(+Nk=ue+KDh zs|zz`$LJ_>c3kPnf0EM@kjNV?+LL-Dspiq8+DDrnFTQ52et2V!mu?8}S6(L!$~o4%+JNW1rG zj_lTqd#77JMy6|M^R;ZO(7P~GSfamCnXNhLYS`(^RdNqb99?jo!=KadBCP%t3$9GlqHg0st@j0YkUmq&r|)}eNs^9v~D^(>v4%Ss|6n) zm~b-lt@EakWj+U1T#{6EJ9w+QGPlFW<-~=vU5d8dr;@J8FX+hT3~xCUp}UU5QTO;H zGi8(W(Gs6xVl(`mp31V_EZn$PYs2fN6^SAj6@A5StoSET7qGtP!$+5mHy24?GjH$P z$5kPvd96jyedB|QvSp^GPZqr1xN)hHQ1guolf%>H=fvpog|vuGn0Z*(#HKkWMBS~^ zLzjCJ$E@Th3hy@DUAl4gqV_>!$97 zRf1WI``A9Hdf0}^3l#26e0-^L|03UBsRylxBs0YuUA7#%@Zs$(OKI$D&CKrZcOBGN^Y*_z80)maO07((~RXWA1H@c zL@iQ%k=b6+apRgE$BWCeeOgkFYpC||bs6?gx@7ad(P+!M=OtdZkIjgmsJX<@w|6~r zy>#!JU3zBW{%69j=t)^0I=rj>j?j@K8k3m97=8u@=v7;A{ zC`oRtoe&=pQ)40RYTBEqc`crQ^GibyQMnmg{xc+%wl-xQV!L#|aklQ-YYwXa4qv!; zT;SY7Gn4sAj?W$+e6!cHC2CIR)?|UN(FeOXu6FydYDMmZmQ3}R*Htp72(580yJW$; z>_y8T?lY!?w)wtT$cE$69iQ_C+ zUFDY3Ig5iQ)=GaU_*hbsyCrDzX6eD;J7QW%hnKcmX^HM|esC@8a4=7?$W>GRJ2N(BK6|+J$)7`_$3Akkd^?rZ z-CQ2$&g!cX^R>J6#hTlvE<9Y8ZM&$ryGl1q>Y(3>*Rz0}2}xCT?7K@FRHco*6WE|DS;+bcs^BW6pg2FY5mp8U?@p6AcYX z=Df9U_KLJ+d}_9J0e89E4Vq8R=xjgCJ$30RlgZyUA2MGV{8RXomfmZI{?{^HXI-|f zkbh?<>O5n9?4r%3A3U3XEYw=PIm;rW(?enr+v(}g#19t=F5my+?3=xhA57@h>HZNl zl~rM5wPys1*$^IfV=d|7L#*}X?=(eXdr)qhV< zU&_q$FX2B!L{qWwg%!)*{ml$c+I1vX=^XqS5+vo9_2@#Je;X6>vJSj?8NMPcs|%-Elr;yGln}@7ra|eV3N`GUi{eT3VUv{qAc6U;>_lM@MnIhzv!8%^PROqOKl%N*`9y&?L%e0Vil`u?ckZ!E*ggKn;4kcBZXEK z3GJS$z3$%A*FiIV)~$|l@v9f%W-M@zXzahK{ba>NM!zF_)}EPcq^G*wg{@UKafYeb zm0Oig3bl@^x*dJI`W2SF>)EFnmh$^(hYB1v;(Zn*Bm3{vr9JO&1g~f?Tr^F0@1a98 z^p09QKge;j!P~elDSg9L#~h!11(&PhSJj$|xMV6#iTu)+nEh&z|C6ezSE_{iZi&ut zobh?7X-#a#)TrRAIUnvnXsUN8k`aHLWT0Bdksq+^s>HsQ=+{-fC$^lOu%-MKcZtr6 z)XF~nOVQ>zXC&VF#csD>w@kp1L34M}Gl@mhWqSWAcukM|*s3~N?(w{jEq)g+Jy^au z;7wEXn#CJydw=wY8@=v$Gksgh%{|FW?`B0*-L-$hQy;;&dF#>48r{>;-`Bn2*z{<| zDzn;6)Rjjn-22PVqNI#M2{JvEA)FyzkH7tFKl@ zT&_zsI4+g)EL`AlX;Hng?1o=c*|jQ;Y|=e!E^7QG>20EzX^41-Cu|d$hY@Ne{jZJL46)nrX-mG#EJsfMOTp1GUzj<}u)ri|N@q*))%^SiE=3ff4 z(W)(vd%NL}33;?f?oWsD}fuQ9h}-W?P;q+kFKH4 zdMVczb2n_6QYdnecSlrTnpob7&Mj}QO&8}DzEZ^*er?SY2aBnTEX>{o#Io04WM8_8 zvru=FrcvwT#mz@WTn-CG?MO}g=%{n)t54a{{>vVF_1i?=UeTU(W2s?o^|ooJ-v;i~ z7X7OI&EC*IgYV<7sl~b){h|7-&)#uGZ(Eh)d`)@9^;z3*hs>KK{-);w^K^Ed94^7i zjDVP1TOB@dI_-bpsJBWp`gls_&f_N}ZfOS;T|KaRXZ9tn$rXQ9`7SMW^uBCcdV6o7 zplJ2-$?iNZo2NJ~&(%DCUdG4Ur=%@VV@gIq^IYvaF`ul>Lw4zL?sZh=R{iB*_A>X6 znzzVCUCmdKIy1g6@S4i$v(jf}u&~fshCA8{tC9lEGFJLn`^xwD*PCoS-`CXTv`k5Z ztv4e7y+C_T#$xA)#*{#=r@JJoCH$UnmZTLQap84)KBvyn?&HV)GQ(Gl8~vV`|C0aX z^0M0?M&Ny*V4hFOZQW_7Q#akr-#1ZLvftvtlwgn25A|o+j&)a8AD;1EE6zde4EM~h zZQZjkAC|H6ZMY(wQgGEhSY+LyLt$IYQl6zOQ%p*)^bTTgW<8eL_H@4MX|^}*-=z0u zH^q8uPWR5uP3($0w%q%|F557d)%q<`LuSb&Gq89B{&%3uj<436e#)|3o zB?q0oyHXc4#vBT(=9bbnlnzw$Im#(}f3NGNli6C4TjaujTPPhYnwo0iWc8SO%|!guK8u0 z!YT{**D{>qne}z?qVL>Z44jb{y7BAEW&^zw60CxMTJ(=_iRfr8x}n5k z^&n7IgkL}|M%TYe)4pee(8`i)eh)Ss3tt(#oa^}>+xM3X45#mT{x>_?O=0P>9Q}+; z-o?%zJB3`gFS_(VQPV9)Fg}n!K-`eoc?y#|Z|0fDoG!~ME^ThO>tOQmU2Mlo-5EjW zDyMb&xS03o{xUV#v(wMxeEfn5XOB1?b8bJ+AL|wfsr;A_uGR77^r8H3A=A%@Jzf{Mae+l;&wqxXha3J(nbEc4;k}4MKQB-A z@n5pDvZpjG=#`aUtVoQsTg)BriyeC|GqP)y?A2Dgbh!3E!&$bojI4goJ2Mu9QQA4>={g%6g5*7Ct&ZA^m3A{qUg8?`jr=aE7vW zH7{~GVk&>jR7Uz#fQavdo#uUyo4v2vxa>V4zhJ)dmSqygtVse4tIX|YeD8be?X_0! z6a(+ic2Dgxje=eAItR8-E!2_``qkUU|Fv^woqOJ*!?lYzGa@I&_83gAJR`d3So@jn zGeTAdq~2uH^jxsC*FI3ZU`3mU%j~Ux+f5DnE*yLmC>0Z(ztAq`z~Tjue%)OpDkR+; z(^+!tGKX1$H;)uonAW<;r&oA|Ql?istw~tXu4EAuEnuXq+`Fs6M_Xs##>H&y3i}i$ ziZ5+c+VHYh`cdJYwY=d;NsAYD&n>&|ly^}Uy!{nG5ENnT3M zy|@7Rv^jT=H2PRwb?a8L%Bf`kd)I3gOR1ssTC*!#UrDQFe_eQTK?wgW>)+>&=Sy`* zaedccTyAtVktb1wwUDhj$XsjB{J#@!FYW90p1vibvEx$-^9_biqy7 zFi_I$J5%HFd3hBVH|Z=nFk{`*JI0(vTMD{eLXR!E^ILaO=N?6i+bR;X=RJEbuOg_s z|AAos+1N#HBE@FhTEgGrRu(+iPy&{vKj zJG`#UUl!MzSuk5mRr4QLw%3hUB^z&DxV`15%eIRPF9t0uIjL~w@ehN*+R}Z3+H0bO z88WM`+lp`N+NzawOWEL&?t~dDn018%8m?H_v%UGlvGKE;Ip6MnS(Y5{=Aa%;gU*gW z&R3nizBWCzS*Xi;W?HG>oA}&o%dA&Sxz^0RbHmHMyiZLGmt0sa^s_-wJ3laIWnZ4| zw|Uad0Pk#bu{j(v6~ zxu+K}FuJhmSG?#F`rys5`y-Q8L-K-fR%eGdRugYFO}(i)iNp67$J%%qmuFrX z#v-XNmhbJ`WGbuk!r4%7%O>7er>C#oYF>3HB<{n{wbKrlg|N#`dB^VD7 zn4Oi^I?KM^(rOJ9%d_>odHUFWN=z5}D?AKz6S8Y`Zb zth>6gDpc9Ni`Q&n?BjrqQ$=leNeGua-d_?TaVBip$1EGeEKB_bmD3)7XyYv3!@0+- zjjK=eRTS?oVPiI>5VIZTi#OgXb9R_;B|Ckd!kSGll8xTZd4J)q?3cSsGYs#xb+tW7 zXW25RPRB90jd#g+{U_#^-bwWx>%Dl-TI$NWh0~WEaa^sewNY+yz&*Xifq8 None: - """Initialize the mock entity.""" - super().__init__() - self._stream_source: str | None = "rtsp://stream" - - def set_stream_source(self, stream_source: str | None) -> None: - """Set the stream source.""" - self._stream_source = stream_source - - async def stream_source(self) -> str | None: - """Return the source of the stream. - - This is used by cameras with CameraEntityFeature.STREAM - and StreamType.HLS. - """ - return self._stream_source - - -@pytest.fixture -def integration_config_entry(hass: HomeAssistant) -> ConfigEntry: - """Test mock config entry.""" - entry = MockConfigEntry(domain=TEST_DOMAIN) - entry.add_to_hass(hass) - return entry - - -@pytest.fixture(name="go2rtc_binary") -def go2rtc_binary_fixture() -> str: - """Fixture to provide go2rtc binary name.""" - return "/usr/bin/go2rtc" - - -@pytest.fixture -def mock_get_binary(go2rtc_binary) -> Generator[Mock]: - """Mock _get_binary.""" - with patch( - "homeassistant.components.go2rtc.shutil.which", - return_value=go2rtc_binary, - ) as mock_which: - yield mock_which - - @pytest.fixture(name="has_go2rtc_entry") def has_go2rtc_entry_fixture() -> bool: """Fixture to control if a go2rtc config entry should be created.""" @@ -126,80 +69,6 @@ def mock_go2rtc_entry(hass: HomeAssistant, has_go2rtc_entry: bool) -> None: config_entry.add_to_hass(hass) -@pytest.fixture(name="is_docker_env") -def is_docker_env_fixture() -> bool: - """Fixture to provide is_docker_env return value.""" - return True - - -@pytest.fixture -def mock_is_docker_env(is_docker_env) -> Generator[Mock]: - """Mock is_docker_env.""" - with patch( - "homeassistant.components.go2rtc.is_docker_env", - return_value=is_docker_env, - ) as mock_is_docker_env: - yield mock_is_docker_env - - -@pytest.fixture -async def init_integration( - hass: HomeAssistant, - rest_client: AsyncMock, - mock_is_docker_env, - mock_get_binary, - server: Mock, -) -> None: - """Initialize the go2rtc integration.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - -@pytest.fixture -async def init_test_integration( - hass: HomeAssistant, - integration_config_entry: ConfigEntry, -) -> MockCamera: - """Initialize components.""" - - async def async_setup_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setups( - config_entry, [Platform.CAMERA] - ) - return True - - async def async_unload_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Unload test config entry.""" - await hass.config_entries.async_forward_entry_unload( - config_entry, Platform.CAMERA - ) - return True - - mock_integration( - hass, - MockModule( - TEST_DOMAIN, - async_setup_entry=async_setup_entry_init, - async_unload_entry=async_unload_entry_init, - ), - ) - test_camera = MockCamera() - setup_test_component_platform( - hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True - ) - mock_platform(hass, f"{TEST_DOMAIN}.config_flow", Mock()) - - with mock_config_flow(TEST_DOMAIN, ConfigFlow): - assert await hass.config_entries.async_setup(integration_config_entry.entry_id) - await hass.async_block_till_done() - - return test_camera - - async def _test_setup_and_signaling( hass: HomeAssistant, issue_registry: ir.IssueRegistry, @@ -218,14 +87,18 @@ async def _test_setup_and_signaling( assert issue_registry.async_get_issue(DOMAIN, "recommended_version") is None config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 - assert config_entries[0].state == ConfigEntryState.LOADED + config_entry = config_entries[0] + assert config_entry.state == ConfigEntryState.LOADED after_setup_fn() receive_message_callback = Mock(spec_set=WebRTCSendMessage) - async def test() -> None: + sessions = [] + + async def test(session: str) -> None: + sessions.append(session) await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback + OFFER_SDP, session, receive_message_callback ) ws_client.send.assert_called_once_with( WebRTCOffer( @@ -240,13 +113,14 @@ async def _test_setup_and_signaling( callback(WebRTCAnswer(ANSWER_SDP)) receive_message_callback.assert_called_once_with(HAWebRTCAnswer(ANSWER_SDP)) - await test() + await test("sesion_1") rest_client.streams.add.assert_called_once_with( entity_id, [ "rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + f"ffmpeg:{camera.entity_id}#video=mjpeg", ], ) @@ -258,13 +132,14 @@ async def _test_setup_and_signaling( receive_message_callback.reset_mock() ws_client.reset_mock() - await test() + await test("session_2") rest_client.streams.add.assert_called_once_with( entity_id, [ "rtsp://stream", f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug", + f"ffmpeg:{camera.entity_id}#video=mjpeg", ], ) @@ -276,25 +151,37 @@ async def _test_setup_and_signaling( receive_message_callback.reset_mock() ws_client.reset_mock() - await test() + await test("session_3") rest_client.streams.add.assert_not_called() assert isinstance(camera._webrtc_provider, WebRTCProvider) - # Set stream source to None and provider should be skipped - rest_client.streams.list.return_value = {} - receive_message_callback.reset_mock() - camera.set_stream_source(None) - await camera.async_handle_async_webrtc_offer( - OFFER_SDP, "session_id", receive_message_callback - ) - receive_message_callback.assert_called_once_with( - WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") - ) + provider = camera._webrtc_provider + for session in sessions: + assert session in provider._sessions + + with patch.object(provider, "teardown", wraps=provider.teardown) as teardown: + # Set stream source to None and provider should be skipped + rest_client.streams.list.return_value = {} + receive_message_callback.reset_mock() + camera.set_stream_source(None) + await camera.async_handle_async_webrtc_offer( + OFFER_SDP, "session_id", receive_message_callback + ) + receive_message_callback.assert_called_once_with( + WebRTCError("go2rtc_webrtc_offer_failed", "Camera has no stream source") + ) + teardown.assert_called_once() + # We use one ws_client mock for all sessions + assert ws_client.close.call_count == len(sessions) + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED + assert teardown.call_count == 2 @pytest.mark.usefixtures( - "init_test_integration", "mock_get_binary", "mock_is_docker_env", "mock_go2rtc_entry", @@ -757,3 +644,29 @@ async def test_setup_with_recommended_version_repair( "recommended_version": RECOMMENDED_VERSION, "current_version": "1.9.5", } + + +@pytest.mark.usefixtures("init_integration") +async def test_async_get_image( + hass: HomeAssistant, + init_test_integration: MockCamera, + rest_client: AsyncMock, +) -> None: + """Test getting snapshot from go2rtc.""" + camera = init_test_integration + assert isinstance(camera._webrtc_provider, WebRTCProvider) + + image_bytes = load_fixture_bytes("snapshot.jpg", DOMAIN) + + rest_client.get_jpeg_snapshot.return_value = image_bytes + assert await camera._webrtc_provider.async_get_image(camera) == image_bytes + + image = await async_get_image(hass, camera.entity_id) + assert image.content == image_bytes + + camera.set_stream_source("invalid://not_supported") + + with pytest.raises( + HomeAssistantError, match="Stream source is not supported by go2rtc" + ): + await async_get_image(hass, camera.entity_id) From 2859e7de9b40ee322d1f0cbc35d7558fc9606c52 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:38:01 +0200 Subject: [PATCH 42/69] Migrate kodi to use runtime_data (#147191) --- homeassistant/components/kodi/__init__.py | 30 +++++++++++-------- homeassistant/components/kodi/const.py | 3 -- homeassistant/components/kodi/media_player.py | 13 ++++---- tests/components/kodi/test_device_trigger.py | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 5400d142f28..5ffde76d313 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1,8 +1,10 @@ """The kodi component.""" +from dataclasses import dataclass import logging from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection +from pykodi.kodi import KodiHTTPConnection, KodiWSConnection from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -17,13 +19,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_WS_PORT, DATA_CONNECTION, DATA_KODI, DOMAIN +from .const import CONF_WS_PORT _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.MEDIA_PLAYER] +type KodiConfigEntry = ConfigEntry[KodiRuntimeData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class KodiRuntimeData: + """Data class to hold Kodi runtime data.""" + + connection: KodiHTTPConnection | KodiWSConnection + kodi: Kodi + + +async def async_setup_entry(hass: HomeAssistant, entry: KodiConfigEntry) -> bool: """Set up Kodi from a config entry.""" conn = get_kodi_connection( entry.data[CONF_HOST], @@ -54,22 +66,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CONNECTION: conn, - DATA_KODI: kodi, - } + entry.runtime_data = KodiRuntimeData(connection=conn, kodi=kodi) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: KodiConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - data = hass.data[DOMAIN].pop(entry.entry_id) - await data[DATA_CONNECTION].close() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await entry.runtime_data.connection.close() return unload_ok diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py index 167ea2a4725..1ac439b27c3 100644 --- a/homeassistant/components/kodi/const.py +++ b/homeassistant/components/kodi/const.py @@ -4,9 +4,6 @@ DOMAIN = "kodi" CONF_WS_PORT = "ws_port" -DATA_CONNECTION = "connection" -DATA_KODI = "kodi" - DEFAULT_PORT = 8080 DEFAULT_SSL = False DEFAULT_TIMEOUT = 5 diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index c4a2436548a..2e32d969fce 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -24,7 +24,7 @@ from homeassistant.components.media_player import ( MediaType, async_process_play_media_url, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -55,6 +55,7 @@ from homeassistant.helpers.network import is_internal_request from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolDictType from homeassistant.util import dt as dt_util +from . import KodiConfigEntry from .browse_media import ( build_item_response, get_media_info, @@ -63,8 +64,6 @@ from .browse_media import ( ) from .const import ( CONF_WS_PORT, - DATA_CONNECTION, - DATA_KODI, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_TIMEOUT, @@ -208,7 +207,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: KodiConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Kodi media player platform.""" @@ -220,14 +219,12 @@ async def async_setup_entry( SERVICE_CALL_METHOD, KODI_CALL_METHOD_SCHEMA, "async_call_method" ) - data = hass.data[DOMAIN][config_entry.entry_id] - connection = data[DATA_CONNECTION] - kodi = data[DATA_KODI] + data = config_entry.runtime_data name = config_entry.data[CONF_NAME] if (uid := config_entry.unique_id) is None: uid = config_entry.entry_id - entity = KodiEntity(connection, kodi, name, uid) + entity = KodiEntity(data.connection, data.kodi, name, uid) async_add_entities([entity]) diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index a54641a4234..541a9f781fd 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.kodi import DOMAIN +from homeassistant.components.kodi.const import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er From 96e0d1f5c6c24ede2b1f9727ed710b75823774b2 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 20 Jun 2025 18:39:43 +1000 Subject: [PATCH 43/69] Fix Charge Cable binary sensor in Teslemetry (#147136) --- homeassistant/components/teslemetry/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index a32c5fea40e..439df76c838 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -126,7 +126,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( polling=True, polling_value_fn=lambda x: x != "", streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType( - lambda value: callback(value != "Unknown") + lambda value: callback(value is not None and value != "Unknown") ), entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, From 3c91c78383973f77dc8f4968491ce2bf046415aa Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:41:25 +0200 Subject: [PATCH 44/69] Use PEP 695 TypeVar syntax for ecovacs (#147153) --- .../components/ecovacs/binary_sensor.py | 9 ++++----- homeassistant/components/ecovacs/entity.py | 20 ++++++++----------- homeassistant/components/ecovacs/number.py | 8 +++----- homeassistant/components/ecovacs/select.py | 10 +++++----- homeassistant/components/ecovacs/sensor.py | 6 ++---- 5 files changed, 22 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py index 7c85a63cc78..32bf5d3ba15 100644 --- a/homeassistant/components/ecovacs/binary_sensor.py +++ b/homeassistant/components/ecovacs/binary_sensor.py @@ -2,9 +2,9 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Generic from deebot_client.capabilities import CapabilityEvent +from deebot_client.events.base import Event from deebot_client.events.water_info import MopAttachedEvent from homeassistant.components.binary_sensor import ( @@ -16,15 +16,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import EcovacsConfigEntry -from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT +from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity from .util import get_supported_entities @dataclass(kw_only=True, frozen=True) -class EcovacsBinarySensorEntityDescription( +class EcovacsBinarySensorEntityDescription[EventT: Event]( BinarySensorEntityDescription, EcovacsCapabilityEntityDescription, - Generic[EventT], ): """Class describing Deebot binary sensor entity.""" @@ -55,7 +54,7 @@ async def async_setup_entry( ) -class EcovacsBinarySensor( +class EcovacsBinarySensor[EventT: Event]( EcovacsDescriptionEntity[CapabilityEvent[EventT]], BinarySensorEntity, ): diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index 36103be4d11..85a788d7afe 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic, TypeVar +from typing import Any from deebot_client.capabilities import Capabilities from deebot_client.device import Device @@ -18,11 +18,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN -CapabilityEntity = TypeVar("CapabilityEntity") -EventT = TypeVar("EventT", bound=Event) - -class EcovacsEntity(Entity, Generic[CapabilityEntity]): +class EcovacsEntity[CapabilityEntityT](Entity): """Ecovacs entity.""" _attr_should_poll = False @@ -32,7 +29,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]): def __init__( self, device: Device, - capability: CapabilityEntity, + capability: CapabilityEntityT, **kwargs: Any, ) -> None: """Initialize entity.""" @@ -80,7 +77,7 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]): self._subscribe(AvailabilityEvent, on_available) - def _subscribe( + def _subscribe[EventT: Event]( self, event_type: type[EventT], callback: Callable[[EventT], Coroutine[Any, Any, None]], @@ -98,13 +95,13 @@ class EcovacsEntity(Entity, Generic[CapabilityEntity]): self._device.events.request_refresh(event_type) -class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]): +class EcovacsDescriptionEntity[CapabilityEntityT](EcovacsEntity[CapabilityEntityT]): """Ecovacs entity.""" def __init__( self, device: Device, - capability: CapabilityEntity, + capability: CapabilityEntityT, entity_description: EntityDescription, **kwargs: Any, ) -> None: @@ -114,13 +111,12 @@ class EcovacsDescriptionEntity(EcovacsEntity[CapabilityEntity]): @dataclass(kw_only=True, frozen=True) -class EcovacsCapabilityEntityDescription( +class EcovacsCapabilityEntityDescription[CapabilityEntityT]( EntityDescription, - Generic[CapabilityEntity], ): """Ecovacs entity description.""" - capability_fn: Callable[[Capabilities], CapabilityEntity | None] + capability_fn: Callable[[Capabilities], CapabilityEntityT | None] class EcovacsLegacyEntity(Entity): diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index 1fbf65aec65..513a0d350f6 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -4,10 +4,10 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic from deebot_client.capabilities import CapabilitySet from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent +from deebot_client.events.base import Event from homeassistant.components.number import ( NumberEntity, @@ -23,16 +23,14 @@ from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, - EventT, ) from .util import get_supported_entities @dataclass(kw_only=True, frozen=True) -class EcovacsNumberEntityDescription( +class EcovacsNumberEntityDescription[EventT: Event]( NumberEntityDescription, EcovacsCapabilityEntityDescription, - Generic[EventT], ): """Ecovacs number entity description.""" @@ -94,7 +92,7 @@ async def async_setup_entry( async_add_entities(entities) -class EcovacsNumberEntity( +class EcovacsNumberEntity[EventT: Event]( EcovacsDescriptionEntity[CapabilitySet[EventT, [int]]], NumberEntity, ): diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index deddb7e252a..84f86fdd2cd 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -2,11 +2,12 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic +from typing import Any from deebot_client.capabilities import CapabilitySetTypes from deebot_client.device import Device from deebot_client.events import WorkModeEvent +from deebot_client.events.base import Event from deebot_client.events.water_info import WaterAmountEvent from homeassistant.components.select import SelectEntity, SelectEntityDescription @@ -15,15 +16,14 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import EcovacsConfigEntry -from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT +from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity from .util import get_name_key, get_supported_entities @dataclass(kw_only=True, frozen=True) -class EcovacsSelectEntityDescription( +class EcovacsSelectEntityDescription[EventT: Event]( SelectEntityDescription, EcovacsCapabilityEntityDescription, - Generic[EventT], ): """Ecovacs select entity description.""" @@ -66,7 +66,7 @@ async def async_setup_entry( async_add_entities(entities) -class EcovacsSelectEntity( +class EcovacsSelectEntity[EventT: Event]( EcovacsDescriptionEntity[CapabilitySetTypes[EventT, [str], str]], SelectEntity, ): diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 98f3783b231..e84485228e4 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Generic +from typing import Any from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType from deebot_client.device import Device @@ -46,16 +46,14 @@ from .entity import ( EcovacsDescriptionEntity, EcovacsEntity, EcovacsLegacyEntity, - EventT, ) from .util import get_name_key, get_options, get_supported_entities @dataclass(kw_only=True, frozen=True) -class EcovacsSensorEntityDescription( +class EcovacsSensorEntityDescription[EventT: Event]( EcovacsCapabilityEntityDescription, SensorEntityDescription, - Generic[EventT], ): """Ecovacs sensor entity description.""" From cd51070219b35d7ca641f04304964969457e8a3e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 11:39:13 +0200 Subject: [PATCH 45/69] Migrate kmtronic to use runtime_data (#147193) --- homeassistant/components/kmtronic/__init__.py | 24 +++++++------------ homeassistant/components/kmtronic/const.py | 3 --- .../components/kmtronic/coordinator.py | 6 ++++- homeassistant/components/kmtronic/switch.py | 10 ++++---- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 1c2cfb7cc31..84959217a5d 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -3,18 +3,16 @@ from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import DATA_COORDINATOR, DATA_HUB, DOMAIN -from .coordinator import KMtronicCoordinator +from .coordinator import KMTronicConfigEntry, KMtronicCoordinator PLATFORMS = [Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool: """Set up kmtronic from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) auth = Auth( @@ -27,11 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = KMtronicCoordinator(hass, entry, hub) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_HUB: hub, - DATA_COORDINATOR: coordinator, - } + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -40,15 +34,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def async_update_options( + hass: HomeAssistant, config_entry: KMTronicConfigEntry +) -> None: """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: KMTronicConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/kmtronic/const.py b/homeassistant/components/kmtronic/const.py index 2381ad57998..6604b559bc2 100644 --- a/homeassistant/components/kmtronic/const.py +++ b/homeassistant/components/kmtronic/const.py @@ -4,7 +4,4 @@ DOMAIN = "kmtronic" CONF_REVERSE = "reverse" -DATA_HUB = "hub" -DATA_COORDINATOR = "coordinator" - MANUFACTURER = "KMtronic" diff --git a/homeassistant/components/kmtronic/coordinator.py b/homeassistant/components/kmtronic/coordinator.py index 8a94949dea6..a5bebff466b 100644 --- a/homeassistant/components/kmtronic/coordinator.py +++ b/homeassistant/components/kmtronic/coordinator.py @@ -18,12 +18,16 @@ PLATFORMS = [Platform.SWITCH] _LOGGER = logging.getLogger(__name__) +type KMTronicConfigEntry = ConfigEntry[KMtronicCoordinator] + class KMtronicCoordinator(DataUpdateCoordinator[None]): """Coordinator for KMTronic.""" + entry: KMTronicConfigEntry + def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, hub: KMTronicHubAPI + self, hass: HomeAssistant, entry: KMTronicConfigEntry, hub: KMTronicHubAPI ) -> None: """Initialize the KMTronic coordinator.""" super().__init__( diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index b32f78b0e98..f8d068cec87 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -4,23 +4,23 @@ from typing import Any import urllib.parse from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_REVERSE, DATA_COORDINATOR, DATA_HUB, DOMAIN, MANUFACTURER +from .const import CONF_REVERSE, DOMAIN, MANUFACTURER +from .coordinator import KMTronicConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: KMTronicConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Config entry example.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - hub = hass.data[DOMAIN][entry.entry_id][DATA_HUB] + coordinator = entry.runtime_data + hub = coordinator.hub reverse = entry.options.get(CONF_REVERSE, False) await hub.async_get_relays() From 544fd2a4a66b3f949c5c84cd14b002f9a84e0c25 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:23:29 +0200 Subject: [PATCH 46/69] Migrate lacrosse_view to use runtime_data (#147202) --- .../components/lacrosse_view/__init__.py | 17 +++++------------ .../components/lacrosse_view/coordinator.py | 6 ++++-- .../components/lacrosse_view/diagnostics.py | 11 +++-------- .../components/lacrosse_view/sensor.py | 11 ++++------- .../lacrosse_view/test_diagnostics.py | 2 +- tests/components/lacrosse_view/test_init.py | 2 -- tests/components/lacrosse_view/test_sensor.py | 8 +------- 7 files changed, 18 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py index e98d1d421be..6cb5e93acfe 100644 --- a/homeassistant/components/lacrosse_view/__init__.py +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -6,20 +6,18 @@ import logging from lacrosse_view import LaCrosse, LoginError -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN -from .coordinator import LaCrosseUpdateCoordinator +from .coordinator import LaCrosseConfigEntry, LaCrosseUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: LaCrosseConfigEntry) -> bool: """Set up LaCrosse View from a config entry.""" api = LaCrosse(async_get_clientsession(hass)) @@ -35,9 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("First refresh") await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "coordinator": coordinator, - } + entry.runtime_data = coordinator _LOGGER.debug("Setting up platforms") await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -45,9 +41,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: LaCrosseConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index 16d7e8b2bb8..1499dd02900 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -17,6 +17,8 @@ from .const import DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) +type LaCrosseConfigEntry = ConfigEntry[LaCrosseUpdateCoordinator] + class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): """DataUpdateCoordinator for LaCrosse View.""" @@ -27,12 +29,12 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): id: str hass: HomeAssistant devices: list[Sensor] | None = None - config_entry: ConfigEntry + config_entry: LaCrosseConfigEntry def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: LaCrosseConfigEntry, api: LaCrosse, ) -> None: """Initialize DataUpdateCoordinator for LaCrosse View.""" diff --git a/homeassistant/components/lacrosse_view/diagnostics.py b/homeassistant/components/lacrosse_view/diagnostics.py index eaf3ded6a4a..479533007c8 100644 --- a/homeassistant/components/lacrosse_view/diagnostics.py +++ b/homeassistant/components/lacrosse_view/diagnostics.py @@ -5,25 +5,20 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import LaCrosseUpdateCoordinator +from .coordinator import LaCrosseConfigEntry TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: LaCrosseConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: LaCrosseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - "coordinator" - ] return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "coordinator_data": coordinator.data, + "coordinator_data": entry.runtime_data.data, } diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index dde8dfd54a2..d0221e22667 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEGREE, PERCENTAGE, @@ -32,6 +31,7 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import DOMAIN +from .coordinator import LaCrosseConfigEntry _LOGGER = logging.getLogger(__name__) @@ -159,17 +159,14 @@ UNIT_OF_MEASUREMENT_MAP = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: LaCrosseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up LaCrosse View from a config entry.""" - coordinator: DataUpdateCoordinator[list[Sensor]] = hass.data[DOMAIN][ - entry.entry_id - ]["coordinator"] - sensors: list[Sensor] = coordinator.data + coordinator = entry.runtime_data sensor_list = [] - for i, sensor in enumerate(sensors): + for i, sensor in enumerate(coordinator.data): for field in sensor.sensor_field_names: description = SENSOR_DESCRIPTIONS.get(field) if description is None: diff --git a/tests/components/lacrosse_view/test_diagnostics.py b/tests/components/lacrosse_view/test_diagnostics.py index 4306173c6b3..0796d3f27f5 100644 --- a/tests/components/lacrosse_view/test_diagnostics.py +++ b/tests/components/lacrosse_view/test_diagnostics.py @@ -5,7 +5,7 @@ from unittest.mock import patch from syrupy.assertion import SnapshotAssertion from syrupy.filters import props -from homeassistant.components.lacrosse_view import DOMAIN +from homeassistant.components.lacrosse_view.const import DOMAIN from homeassistant.core import HomeAssistant from . import MOCK_ENTRY_DATA, TEST_SENSOR diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index 0533dd2abee..3691ee1c7ac 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -35,8 +35,6 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] - entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index e0dc1e5f35f..f0860f47b01 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import patch from lacrosse_view import Sensor import pytest -from homeassistant.components.lacrosse_view import DOMAIN +from homeassistant.components.lacrosse_view.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -46,7 +46,6 @@ async def test_entities_added(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -103,7 +102,6 @@ async def test_field_not_supported( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -144,7 +142,6 @@ async def test_field_types( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -172,7 +169,6 @@ async def test_no_field(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -200,7 +196,6 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -228,7 +223,6 @@ async def test_no_readings(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 From 7dfd68f8c05ec1e410eaaa84f6cd4ca9dad02c89 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:23:59 +0200 Subject: [PATCH 47/69] Migrate keenetic_ndms2 to use runtime_data (#147194) * Migrate keenetic_ndms2 to use runtime_data * Adjust tests --- .../components/keenetic_ndms2/__init__.py | 23 ++++++++----------- .../keenetic_ndms2/binary_sensor.py | 10 +++----- .../components/keenetic_ndms2/config_flow.py | 18 +++++---------- .../components/keenetic_ndms2/const.py | 1 - .../keenetic_ndms2/device_tracker.py | 8 +++---- .../components/keenetic_ndms2/router.py | 4 +++- .../keenetic_ndms2/test_config_flow.py | 19 +++++++-------- 7 files changed, 32 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index a4447dcd904..7986158ab50 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -19,15 +18,14 @@ from .const import ( DEFAULT_INTERFACE, DEFAULT_SCAN_INTERVAL, DOMAIN, - ROUTER, ) -from .router import KeeneticRouter +from .router import KeeneticConfigEntry, KeeneticRouter PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: KeeneticConfigEntry) -> bool: """Set up the component.""" hass.data.setdefault(DOMAIN, {}) async_add_defaults(hass, entry) @@ -37,27 +35,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.data[DOMAIN][entry.entry_id] = { - ROUTER: router, - } + entry.runtime_data = router await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: KeeneticConfigEntry +) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS ) - router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] - + router = config_entry.runtime_data await router.async_teardown() - hass.data[DOMAIN].pop(config_entry.entry_id) - new_tracked_interfaces: set[str] = set(config_entry.options[CONF_INTERFACES]) if router.tracked_interfaces - new_tracked_interfaces: @@ -92,12 +87,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def update_listener(hass: HomeAssistant, entry: KeeneticConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -def async_add_defaults(hass: HomeAssistant, entry: ConfigEntry): +def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry): """Populate default options.""" host: str = entry.data[CONF_HOST] imported_options: dict = hass.data[DOMAIN].get(f"imported_options_{host}", {}) diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index 4d1b5da3552..6eea55c33e7 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -4,24 +4,20 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import KeeneticRouter -from .const import DOMAIN, ROUTER +from .router import KeeneticConfigEntry, KeeneticRouter async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: KeeneticConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up device tracker for Keenetic NDMS2 component.""" - router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] - - async_add_entities([RouterOnlineBinarySensor(router)]) + async_add_entities([RouterOnlineBinarySensor(config_entry.runtime_data)]) class RouterOnlineBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 3dc4c8b1b77..7219819b911 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -8,12 +8,7 @@ from urllib.parse import urlparse from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -41,9 +36,8 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_TELNET_PORT, DOMAIN, - ROUTER, ) -from .router import KeeneticRouter +from .router import KeeneticConfigEntry class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN): @@ -56,7 +50,7 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: KeeneticConfigEntry, ) -> KeeneticOptionsFlowHandler: """Get the options flow for this handler.""" return KeeneticOptionsFlowHandler() @@ -142,6 +136,8 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN): class KeeneticOptionsFlowHandler(OptionsFlow): """Handle options.""" + config_entry: KeeneticConfigEntry + def __init__(self) -> None: """Initialize options flow.""" self._interface_options: dict[str, str] = {} @@ -150,9 +146,7 @@ class KeeneticOptionsFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Manage the options.""" - router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][ - ROUTER - ] + router = self.config_entry.runtime_data interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job( router.client.get_interfaces diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py index d7db0673690..4a856647387 100644 --- a/homeassistant/components/keenetic_ndms2/const.py +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -5,7 +5,6 @@ from homeassistant.components.device_tracker import ( ) DOMAIN = "keenetic_ndms2" -ROUTER = "router" DEFAULT_TELNET_PORT = 23 DEFAULT_SCAN_INTERVAL = 120 DEFAULT_CONSIDER_HOME = _DEFAULT_CONSIDER_HOME.total_seconds() diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 4143611d6af..7de7c497ef3 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -10,26 +10,24 @@ from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, ScannerEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN, ROUTER -from .router import KeeneticRouter +from .router import KeeneticConfigEntry, KeeneticRouter _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: KeeneticConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up device tracker for Keenetic NDMS2 component.""" - router: KeeneticRouter = hass.data[DOMAIN][config_entry.entry_id][ROUTER] + router = config_entry.runtime_data tracked: set[str] = set() diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index 8c3079b910d..364e921cd40 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -35,11 +35,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type KeeneticConfigEntry = ConfigEntry[KeeneticRouter] + class KeeneticRouter: """Keenetic client Object.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, config_entry: KeeneticConfigEntry) -> None: """Initialize the Client.""" self.hass = hass self.config_entry = config_entry diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 7ddcdf38ed6..c8e23786e68 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -87,19 +87,16 @@ async def test_options(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 # fake router - hass.data.setdefault(keenetic.DOMAIN, {}) - hass.data[keenetic.DOMAIN][entry.entry_id] = { - keenetic.ROUTER: Mock( - client=Mock( - get_interfaces=Mock( - return_value=[ - InterfaceInfo.from_dict({"id": name, "type": "bridge"}) - for name in MOCK_OPTIONS[const.CONF_INTERFACES] - ] - ) + entry.runtime_data = Mock( + client=Mock( + get_interfaces=Mock( + return_value=[ + InterfaceInfo.from_dict({"id": name, "type": "bridge"}) + for name in MOCK_OPTIONS[const.CONF_INTERFACES] + ] ) ) - } + ) result = await hass.config_entries.options.async_init(entry.entry_id) From 313eaff14e927193520d9ae4616fefa85e411723 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:25:57 +0200 Subject: [PATCH 48/69] Migrate kaleidescape to use runtime_data (#147171) * Migrate kaleidescape to use runtime_data * Adjust tests --- .../components/kaleidescape/__init__.py | 30 ++++++++----------- .../components/kaleidescape/media_player.py | 18 ++++------- .../components/kaleidescape/remote.py | 19 +++++------- .../components/kaleidescape/sensor.py | 23 ++++++-------- tests/components/kaleidescape/test_init.py | 2 -- 5 files changed, 35 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py index f074ac640d8..c6639e096d7 100644 --- a/homeassistant/components/kaleidescape/__init__.py +++ b/homeassistant/components/kaleidescape/__init__.py @@ -3,26 +3,22 @@ from __future__ import annotations from dataclasses import dataclass -import logging -from typing import TYPE_CHECKING from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from .const import DOMAIN - -if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntry - from homeassistant.core import Event, HomeAssistant - -_LOGGER = logging.getLogger(__name__) - PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR] +type KaleidescapeConfigEntry = ConfigEntry[KaleidescapeDevice] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, entry: KaleidescapeConfigEntry +) -> bool: """Set up Kaleidescape from a config entry.""" device = KaleidescapeDevice( entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5 @@ -36,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Unable to connect to {entry.data[CONF_HOST]}: {err}" ) from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + entry.runtime_data = device async def disconnect(event: Event) -> None: await device.disconnect() @@ -44,18 +40,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) ) + entry.async_on_unload(device.disconnect) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: KaleidescapeConfigEntry +) -> bool: """Unload config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - await hass.data[DOMAIN][entry.entry_id].disconnect() - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @dataclass diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py index cd8aa9d4a8e..564b0c41c30 100644 --- a/homeassistant/components/kaleidescape/media_player.py +++ b/homeassistant/components/kaleidescape/media_player.py @@ -2,8 +2,8 @@ from __future__ import annotations +from datetime import datetime import logging -from typing import TYPE_CHECKING from kaleidescape import const as kaleidescape_const @@ -12,19 +12,13 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, MediaPlayerState, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.dt import utcnow -from .const import DOMAIN +from . import KaleidescapeConfigEntry from .entity import KaleidescapeEntity -if TYPE_CHECKING: - from datetime import datetime - - from homeassistant.config_entries import ConfigEntry - from homeassistant.core import HomeAssistant - from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - - KALEIDESCAPE_PLAYING_STATES = [ kaleidescape_const.PLAY_STATUS_PLAYING, kaleidescape_const.PLAY_STATUS_FORWARD, @@ -39,11 +33,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: KaleidescapeConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform from a config entry.""" - entities = [KaleidescapeMediaPlayer(hass.data[DOMAIN][entry.entry_id])] + entities = [KaleidescapeMediaPlayer(entry.runtime_data)] async_add_entities(entities) diff --git a/homeassistant/components/kaleidescape/remote.py b/homeassistant/components/kaleidescape/remote.py index 2b341e0c429..a71fb7f917a 100644 --- a/homeassistant/components/kaleidescape/remote.py +++ b/homeassistant/components/kaleidescape/remote.py @@ -2,32 +2,27 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Iterable +from typing import Any from kaleidescape import const as kaleidescape_const from homeassistant.components.remote import RemoteEntity +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from . import KaleidescapeConfigEntry from .entity import KaleidescapeEntity -if TYPE_CHECKING: - from collections.abc import Iterable - from typing import Any - - from homeassistant.config_entries import ConfigEntry - from homeassistant.core import HomeAssistant - from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: KaleidescapeConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform from a config entry.""" - entities = [KaleidescapeRemote(hass.data[DOMAIN][entry.entry_id])] + entities = [KaleidescapeRemote(entry.runtime_data)] async_add_entities(entities) diff --git a/homeassistant/components/kaleidescape/sensor.py b/homeassistant/components/kaleidescape/sensor.py index ac0f6504daa..8d7365aa20b 100644 --- a/homeassistant/components/kaleidescape/sensor.py +++ b/homeassistant/components/kaleidescape/sensor.py @@ -2,25 +2,20 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING + +from kaleidescape import Device as KaleidescapeDevice from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from . import KaleidescapeConfigEntry from .entity import KaleidescapeEntity -if TYPE_CHECKING: - from collections.abc import Callable - - from kaleidescape import Device as KaleidescapeDevice - - from homeassistant.config_entries import ConfigEntry - from homeassistant.core import HomeAssistant - from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - from homeassistant.helpers.typing import StateType - @dataclass(frozen=True, kw_only=True) class KaleidescapeSensorEntityDescription(SensorEntityDescription): @@ -132,11 +127,11 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: KaleidescapeConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform from a config entry.""" - device: KaleidescapeDevice = hass.data[DOMAIN][entry.entry_id] + device = entry.runtime_data async_add_entities( KaleidescapeSensor(device, description) for description in SENSOR_TYPES ) diff --git a/tests/components/kaleidescape/test_init.py b/tests/components/kaleidescape/test_init.py index 01769b9fc57..ed1a9981906 100644 --- a/tests/components/kaleidescape/test_init.py +++ b/tests/components/kaleidescape/test_init.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock import pytest -from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -29,7 +28,6 @@ async def test_unload_config_entry( await hass.async_block_till_done() assert mock_device.disconnect.call_count == 1 - assert mock_config_entry.entry_id not in hass.data[DOMAIN] async def test_config_entry_not_ready( From 1b60ea8951753fd2fcbadaf63ea5e166d047942f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 Jun 2025 12:26:07 +0200 Subject: [PATCH 49/69] Migrate lutron to use runtime_data (#147198) --- homeassistant/components/lutron/__init__.py | 10 +++++++--- homeassistant/components/lutron/binary_sensor.py | 10 +++------- homeassistant/components/lutron/config_flow.py | 10 +++------- homeassistant/components/lutron/cover.py | 7 +++---- homeassistant/components/lutron/event.py | 7 +++---- homeassistant/components/lutron/fan.py | 10 +++------- homeassistant/components/lutron/light.py | 6 +++--- homeassistant/components/lutron/scene.py | 7 +++---- homeassistant/components/lutron/switch.py | 7 +++---- 9 files changed, 31 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 6ea3754ddde..97823d404fc 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -29,6 +29,8 @@ ATTR_ACTION = "action" ATTR_FULL_ID = "full_id" ATTR_UUID = "uuid" +type LutronConfigEntry = ConfigEntry[LutronData] + @dataclass(slots=True, kw_only=True) class LutronData: @@ -44,7 +46,9 @@ class LutronData: switches: list[tuple[str, Output]] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: LutronConfigEntry +) -> bool: """Set up the Lutron integration.""" host = config_entry.data[CONF_HOST] @@ -169,7 +173,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b name="Main repeater", ) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = entry_data + config_entry.runtime_data = entry_data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -222,6 +226,6 @@ def _async_check_device_identifiers( ) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: LutronConfigEntry) -> bool: """Clean up resources and entities associated with the integration.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index 5bed760e1ac..fddfdac7c8d 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Mapping -import logging from typing import Any from pylutron import OccupancyGroup @@ -12,19 +11,16 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, LutronData +from . import LutronConfigEntry from .entity import LutronDevice -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron binary_sensor platform. @@ -32,7 +28,7 @@ async def async_setup_entry( Adds occupancy groups from the Main Repeater associated with the config_entry as binary_sensor entities. """ - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data async_add_entities( [ LutronOccupancySensor(area_name, device, entry_data.client) diff --git a/homeassistant/components/lutron/config_flow.py b/homeassistant/components/lutron/config_flow.py index 3f55a2b131b..bd1cd107e8c 100644 --- a/homeassistant/components/lutron/config_flow.py +++ b/homeassistant/components/lutron/config_flow.py @@ -9,12 +9,7 @@ from urllib.error import HTTPError from pylutron import Lutron import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers.selector import ( @@ -23,6 +18,7 @@ from homeassistant.helpers.selector import ( NumberSelectorMode, ) +from . import LutronConfigEntry from .const import CONF_DEFAULT_DIMMER_LEVEL, DEFAULT_DIMMER_LEVEL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -83,7 +79,7 @@ class LutronConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler() diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index e8f3ad09879..8909e49f7aa 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -13,11 +13,10 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, LutronData +from . import LutronConfigEntry from .entity import LutronDevice _LOGGER = logging.getLogger(__name__) @@ -25,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron cover platform. @@ -33,7 +32,7 @@ async def async_setup_entry( Adds shades from the Main Repeater associated with the config_entry as cover entities. """ - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data async_add_entities( [ LutronCover(area_name, device, entry_data.client) diff --git a/homeassistant/components/lutron/event.py b/homeassistant/components/lutron/event.py index 942e165b97f..d7ec85835b7 100644 --- a/homeassistant/components/lutron/event.py +++ b/homeassistant/components/lutron/event.py @@ -5,13 +5,12 @@ from enum import StrEnum from pylutron import Button, Keypad, Lutron, LutronEvent from homeassistant.components.event import EventEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import slugify -from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, DOMAIN, LutronData +from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, LutronConfigEntry from .entity import LutronKeypad @@ -32,11 +31,11 @@ LEGACY_EVENT_TYPES: dict[LutronEventType, str] = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron event platform.""" - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data async_add_entities( LutronEventEntity(area_name, keypad, button, entry_data.client) diff --git a/homeassistant/components/lutron/fan.py b/homeassistant/components/lutron/fan.py index 5928c3c2da3..cc63994cdbe 100644 --- a/homeassistant/components/lutron/fan.py +++ b/homeassistant/components/lutron/fan.py @@ -2,25 +2,21 @@ from __future__ import annotations -import logging from typing import Any from pylutron import Output from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, LutronData +from . import LutronConfigEntry from .entity import LutronDevice -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron fan platform. @@ -28,7 +24,7 @@ async def async_setup_entry( Adds fan controls from the Main Repeater associated with the config_entry as fan entities. """ - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data async_add_entities( [ LutronFan(area_name, device, entry_data.client) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index a7489e13b7b..955c4a2af90 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -19,14 +19,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, LutronData +from . import LutronConfigEntry from .const import CONF_DEFAULT_DIMMER_LEVEL, DEFAULT_DIMMER_LEVEL from .entity import LutronDevice async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron light platform. @@ -34,7 +34,7 @@ async def async_setup_entry( Adds dimmers from the Main Repeater associated with the config_entry as light entities. """ - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data async_add_entities( ( diff --git a/homeassistant/components/lutron/scene.py b/homeassistant/components/lutron/scene.py index 4889f9056ac..5f3736f0882 100644 --- a/homeassistant/components/lutron/scene.py +++ b/homeassistant/components/lutron/scene.py @@ -7,17 +7,16 @@ from typing import Any from pylutron import Button, Keypad, Lutron from homeassistant.components.scene import Scene -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, LutronData +from . import LutronConfigEntry from .entity import LutronKeypad async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron scene platform. @@ -25,7 +24,7 @@ async def async_setup_entry( Adds scenes from the Main Repeater associated with the config_entry as scene entities. """ - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data async_add_entities( LutronScene(area_name, keypad, device, entry_data.client) diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index e1e97d1774a..addde6f95aa 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -8,17 +8,16 @@ from typing import Any from pylutron import Button, Keypad, Led, Lutron, Output from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN, LutronData +from . import LutronConfigEntry from .entity import LutronDevice, LutronKeypad async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: LutronConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Lutron switch platform. @@ -26,7 +25,7 @@ async def async_setup_entry( Adds switches from the Main Repeater associated with the config_entry as switch entities. """ - entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + entry_data = config_entry.runtime_data entities: list[SwitchEntity] = [] # Add Lutron Switches From 9ae9ad1e43b0228b8d291a1bfba32cb1a2347921 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Fri, 20 Jun 2025 12:28:49 +0200 Subject: [PATCH 50/69] Improve test-coverage for homee locks (#147160) test for unknown user --- tests/components/homee/test_lock.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/components/homee/test_lock.py b/tests/components/homee/test_lock.py index 3e6ff3f8ec6..6f41185c4ed 100644 --- a/tests/components/homee/test_lock.py +++ b/tests/components/homee/test_lock.py @@ -111,6 +111,23 @@ async def test_lock_changed_by( assert hass.states.get("lock.test_lock").attributes["changed_by"] == expected +async def test_lock_changed_by_unknown_user( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homee: MagicMock, +) -> None: + """Test lock changed by entries.""" + mock_homee.nodes = [build_mock_node("lock.json")] + mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] + mock_homee.get_user_by_id.return_value = None # Simulate unknown user + attribute = mock_homee.nodes[0].attributes[0] + attribute.changed_by = 2 + attribute.changed_by_id = 1 + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("lock.test_lock").attributes["changed_by"] == "user-Unknown" + + async def test_lock_snapshot( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From a493bdc20817deaaed48b71509961c617cf755a0 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:19:45 +0200 Subject: [PATCH 51/69] Implement battery group mode in HomeWizard (#146770) * Implement battery group mode for HomeWizard P1 * Clean up test * Disable 'entity_registry_enabled_default' * Fix failing tests because of 'entity_registry_enabled_default' * Proof entities are disabled by default * Undo dev change * Update homeassistant/components/homewizard/select.py * Update homeassistant/components/homewizard/select.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/homewizard/strings.json * Apply suggestions from code review * Update tests due to updated translations --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/homewizard/const.py | 8 +- .../components/homewizard/helpers.py | 7 +- homeassistant/components/homewizard/select.py | 89 ++++++ .../components/homewizard/strings.json | 15 +- tests/components/homewizard/conftest.py | 15 + .../homewizard/fixtures/HWE-P1/batteries.json | 7 + .../snapshots/test_diagnostics.ambr | 8 +- .../homewizard/snapshots/test_select.ambr | 97 ++++++ tests/components/homewizard/test_button.py | 2 +- tests/components/homewizard/test_number.py | 2 +- tests/components/homewizard/test_select.py | 294 ++++++++++++++++++ tests/components/homewizard/test_switch.py | 4 +- 12 files changed, 540 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/homewizard/select.py create mode 100644 tests/components/homewizard/fixtures/HWE-P1/batteries.json create mode 100644 tests/components/homewizard/snapshots/test_select.ambr create mode 100644 tests/components/homewizard/test_select.py diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index e0448edaf86..ed1c140a23b 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -8,7 +8,13 @@ import logging from homeassistant.const import Platform DOMAIN = "homewizard" -PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BUTTON, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/homewizard/helpers.py b/homeassistant/components/homewizard/helpers.py index c4160b0bbb0..0aee8f80078 100644 --- a/homeassistant/components/homewizard/helpers.py +++ b/homeassistant/components/homewizard/helpers.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from typing import Any, Concatenate -from homewizard_energy.errors import DisabledError, RequestError +from homewizard_energy.errors import DisabledError, RequestError, UnauthorizedError from homeassistant.exceptions import HomeAssistantError @@ -41,5 +41,10 @@ def homewizard_exception_handler[_HomeWizardEntityT: HomeWizardEntity, **_P]( translation_domain=DOMAIN, translation_key="api_disabled", ) from ex + except UnauthorizedError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_unauthorized", + ) from ex return handler diff --git a/homeassistant/components/homewizard/select.py b/homeassistant/components/homewizard/select.py new file mode 100644 index 00000000000..2ae37883107 --- /dev/null +++ b/homeassistant/components/homewizard/select.py @@ -0,0 +1,89 @@ +"""Support for HomeWizard select platform.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from homewizard_energy import HomeWizardEnergy +from homewizard_energy.models import Batteries, CombinedModels as DeviceResponseEntry + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class HomeWizardSelectEntityDescription(SelectEntityDescription): + """Class describing HomeWizard select entities.""" + + available_fn: Callable[[DeviceResponseEntry], bool] + create_fn: Callable[[DeviceResponseEntry], bool] + current_fn: Callable[[DeviceResponseEntry], str | None] + set_fn: Callable[[HomeWizardEnergy, str], Awaitable[Any]] + + +DESCRIPTIONS = [ + HomeWizardSelectEntityDescription( + key="battery_group_mode", + translation_key="battery_group_mode", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + options=[Batteries.Mode.ZERO, Batteries.Mode.STANDBY, Batteries.Mode.TO_FULL], + available_fn=lambda x: x.batteries is not None, + create_fn=lambda x: x.batteries is not None, + current_fn=lambda x: x.batteries.mode if x.batteries else None, + set_fn=lambda api, mode: api.batteries(mode=Batteries.Mode(mode)), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HomeWizardConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up HomeWizard select based on a config entry.""" + async_add_entities( + HomeWizardSelectEntity( + coordinator=entry.runtime_data, + description=description, + ) + for description in DESCRIPTIONS + if description.create_fn(entry.runtime_data.data) + ) + + +class HomeWizardSelectEntity(HomeWizardEntity, SelectEntity): + """Defines a HomeWizard select entity.""" + + entity_description: HomeWizardSelectEntityDescription + + def __init__( + self, + coordinator: HWEnergyDeviceUpdateCoordinator, + description: HomeWizardSelectEntityDescription, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self.entity_description.current_fn(self.coordinator.data) + + @homewizard_exception_handler + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + await self.entity_description.set_fn(self.coordinator.api, option) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/homewizard/strings.json b/homeassistant/components/homewizard/strings.json index 076e9375d24..4216ece64cb 100644 --- a/homeassistant/components/homewizard/strings.json +++ b/homeassistant/components/homewizard/strings.json @@ -152,14 +152,27 @@ "cloud_connection": { "name": "Cloud connection" } + }, + "select": { + "battery_group_mode": { + "name": "Battery group mode", + "state": { + "zero": "Zero mode", + "to_full": "Manual charge mode", + "standby": "Standby" + } + } } }, "exceptions": { "api_disabled": { "message": "The local API is disabled." }, + "api_unauthorized": { + "message": "The local API is unauthorized. Restore API access by following the instructions in the repair issue." + }, "communication_error": { - "message": "An error occurred while communicating with HomeWizard device" + "message": "An error occurred while communicating with your HomeWizard Energy device" } }, "issues": { diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index b8367f87e57..c6098342d25 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch from homewizard_energy.models import ( + Batteries, CombinedModels, Device, Measurement, @@ -64,6 +65,13 @@ def mock_homewizardenergy( if get_fixture_path(f"{device_fixture}/system.json", DOMAIN).exists() else None ), + batteries=( + Batteries.from_dict( + load_json_object_fixture(f"{device_fixture}/batteries.json", DOMAIN) + ) + if get_fixture_path(f"{device_fixture}/batteries.json", DOMAIN).exists() + else None + ), ) # device() call is used during configuration flow @@ -112,6 +120,13 @@ def mock_homewizardenergy_v2( if get_fixture_path(f"v2/{device_fixture}/system.json", DOMAIN).exists() else None ), + batteries=( + Batteries.from_dict( + load_json_object_fixture(f"{device_fixture}/batteries.json", DOMAIN) + ) + if get_fixture_path(f"{device_fixture}/batteries.json", DOMAIN).exists() + else None + ), ) # device() call is used during configuration flow diff --git a/tests/components/homewizard/fixtures/HWE-P1/batteries.json b/tests/components/homewizard/fixtures/HWE-P1/batteries.json new file mode 100644 index 00000000000..279e49606b3 --- /dev/null +++ b/tests/components/homewizard/fixtures/HWE-P1/batteries.json @@ -0,0 +1,7 @@ +{ + "mode": "zero", + "power_w": -404, + "target_power_w": -400, + "max_consumption_w": 1600, + "max_production_w": 800 +} diff --git a/tests/components/homewizard/snapshots/test_diagnostics.ambr b/tests/components/homewizard/snapshots/test_diagnostics.ambr index c8addf72368..449dfd0c02f 100644 --- a/tests/components/homewizard/snapshots/test_diagnostics.ambr +++ b/tests/components/homewizard/snapshots/test_diagnostics.ambr @@ -278,7 +278,13 @@ # name: test_diagnostics[HWE-P1] dict({ 'data': dict({ - 'batteries': None, + 'batteries': dict({ + 'max_consumption_w': 1600.0, + 'max_production_w': 800.0, + 'mode': 'zero', + 'power_w': -404.0, + 'target_power_w': -400.0, + }), 'device': dict({ 'api_version': '1.0.0', 'firmware_version': '4.19', diff --git a/tests/components/homewizard/snapshots/test_select.ambr b/tests/components/homewizard/snapshots/test_select.ambr new file mode 100644 index 00000000000..ecfd80e04da --- /dev/null +++ b/tests/components/homewizard/snapshots/test_select.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_select_entity_snapshots[HWE-P1-select.device_battery_group_mode] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Battery group mode', + 'options': list([ + , + , + , + ]), + }), + 'context': , + 'entity_id': 'select.device_battery_group_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'zero', + }) +# --- +# name: test_select_entity_snapshots[HWE-P1-select.device_battery_group_mode].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.device_battery_group_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery group mode', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_group_mode', + 'unique_id': 'HWE-P1_5c2fafabcdef_battery_group_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select_entity_snapshots[HWE-P1-select.device_battery_group_mode].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '5c:2f:af:ab:cd:ef', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '5c2fafabcdef', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'Wi-Fi P1 Meter', + 'model_id': 'HWE-P1', + 'name': 'Device', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.19', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/homewizard/test_button.py b/tests/components/homewizard/test_button.py index d0a6d92b36f..f5c28735da4 100644 --- a/tests/components/homewizard/test_button.py +++ b/tests/components/homewizard/test_button.py @@ -61,7 +61,7 @@ async def test_identify_button( with pytest.raises( HomeAssistantError, - match=r"^An error occurred while communicating with HomeWizard device$", + match=r"^An error occurred while communicating with your HomeWizard Energy device$", ): await hass.services.async_call( button.DOMAIN, diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py index 67e51cbafe2..ffc31cb3859 100644 --- a/tests/components/homewizard/test_number.py +++ b/tests/components/homewizard/test_number.py @@ -73,7 +73,7 @@ async def test_number_entities( mock_homewizardenergy.system.side_effect = RequestError with pytest.raises( HomeAssistantError, - match=r"^An error occurred while communicating with HomeWizard device$", + match=r"^An error occurred while communicating with your HomeWizard Energy device$", ): await hass.services.async_call( number.DOMAIN, diff --git a/tests/components/homewizard/test_select.py b/tests/components/homewizard/test_select.py new file mode 100644 index 00000000000..d61f8d167c4 --- /dev/null +++ b/tests/components/homewizard/test_select.py @@ -0,0 +1,294 @@ +"""Test the Select entity for HomeWizard.""" + +from unittest.mock import MagicMock + +from homewizard_energy import UnsupportedError +from homewizard_energy.errors import RequestError, UnauthorizedError +from homewizard_energy.models import Batteries +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.homewizard.const import UPDATE_INTERVAL +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed + +pytestmark = [ + pytest.mark.usefixtures("init_integration"), +] + + +@pytest.mark.parametrize( + ("device_fixture", "entity_ids"), + [ + ( + "HWE-WTR", + [ + "select.device_battery_group_mode", + ], + ), + ( + "SDM230", + [ + "select.device_battery_group_mode", + ], + ), + ( + "SDM630", + [ + "select.device_battery_group_mode", + ], + ), + ( + "HWE-KWH1", + [ + "select.device_battery_group_mode", + ], + ), + ( + "HWE-KWH3", + [ + "select.device_battery_group_mode", + ], + ), + ], +) +async def test_entities_not_created_for_device( + hass: HomeAssistant, + entity_ids: list[str], +) -> None: + """Ensures entities for a specific device are not created.""" + for entity_id in entity_ids: + assert not hass.states.get(entity_id) + + +@pytest.mark.parametrize( + ("device_fixture", "entity_id"), + [ + ("HWE-P1", "select.device_battery_group_mode"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_entity_snapshots( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + entity_id: str, +) -> None: + """Test that select entity state and registry entries match snapshots.""" + assert (state := hass.states.get(entity_id)) + assert snapshot == state + + assert (entity_entry := entity_registry.async_get(entity_id)) + assert snapshot == entity_entry + + assert entity_entry.device_id + assert (device_entry := device_registry.async_get(entity_entry.device_id)) + assert snapshot == device_entry + + +@pytest.mark.parametrize( + ("device_fixture", "entity_id", "option", "expected_mode"), + [ + ( + "HWE-P1", + "select.device_battery_group_mode", + "standby", + Batteries.Mode.STANDBY, + ), + ( + "HWE-P1", + "select.device_battery_group_mode", + "to_full", + Batteries.Mode.TO_FULL, + ), + ("HWE-P1", "select.device_battery_group_mode", "zero", Batteries.Mode.ZERO), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_set_option( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + entity_id: str, + option: str, + expected_mode: Batteries.Mode, +) -> None: + """Test that selecting an option calls the correct API method.""" + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: option, + }, + blocking=True, + ) + mock_homewizardenergy.batteries.assert_called_with(mode=expected_mode) + + +@pytest.mark.parametrize( + ("device_fixture", "entity_id", "option"), + [ + ("HWE-P1", "select.device_battery_group_mode", "zero"), + ("HWE-P1", "select.device_battery_group_mode", "standby"), + ("HWE-P1", "select.device_battery_group_mode", "to_full"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_request_error( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + entity_id: str, + option: str, +) -> None: + """Test that RequestError is handled and raises HomeAssistantError.""" + mock_homewizardenergy.batteries.side_effect = RequestError + with pytest.raises( + HomeAssistantError, + match=r"^An error occurred while communicating with your HomeWizard Energy device$", + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: option, + }, + blocking=True, + ) + + +@pytest.mark.parametrize( + ("device_fixture", "entity_id", "option"), + [ + ("HWE-P1", "select.device_battery_group_mode", "to_full"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_unauthorized_error( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + entity_id: str, + option: str, +) -> None: + """Test that UnauthorizedError is handled and raises HomeAssistantError.""" + mock_homewizardenergy.batteries.side_effect = UnauthorizedError + with pytest.raises( + HomeAssistantError, + match=r"^The local API is unauthorized\. Restore API access by following the instructions in the repair issue$", + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: option, + }, + blocking=True, + ) + + +@pytest.mark.parametrize("device_fixture", ["HWE-P1"]) +@pytest.mark.parametrize("exception", [RequestError, UnsupportedError]) +@pytest.mark.parametrize( + ("entity_id", "method"), + [ + ("select.device_battery_group_mode", "combined"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_unreachable( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + exception: Exception, + entity_id: str, + method: str, +) -> None: + """Test that unreachable devices are marked as unavailable.""" + mocked_method = getattr(mock_homewizardenergy, method) + mocked_method.side_effect = exception + async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("device_fixture", "entity_id"), + [ + ("HWE-P1", "select.device_battery_group_mode"), + ], +) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_select_multiple_state_changes( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + entity_id: str, +) -> None: + """Test changing select state multiple times in sequence.""" + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "zero", + }, + blocking=True, + ) + mock_homewizardenergy.batteries.assert_called_with(mode=Batteries.Mode.ZERO) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "to_full", + }, + blocking=True, + ) + mock_homewizardenergy.batteries.assert_called_with(mode=Batteries.Mode.TO_FULL) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: "standby", + }, + blocking=True, + ) + mock_homewizardenergy.batteries.assert_called_with(mode=Batteries.Mode.STANDBY) + + +@pytest.mark.parametrize( + ("device_fixture", "entity_ids"), + [ + ( + "HWE-P1", + [ + "select.device_battery_group_mode", + ], + ), + ], +) +async def test_disabled_by_default_selects( + hass: HomeAssistant, entity_registry: er.EntityRegistry, entity_ids: list[str] +) -> None: + """Test the disabled by default selects.""" + for entity_id in entity_ids: + assert not hass.states.get(entity_id) + + assert (entry := entity_registry.async_get(entity_id)) + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index ae9b7653b6d..9eba571273d 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -149,7 +149,7 @@ async def test_switch_entities( with pytest.raises( HomeAssistantError, - match=r"^An error occurred while communicating with HomeWizard device$", + match=r"^An error occurred while communicating with your HomeWizard Energy device$", ): await hass.services.async_call( switch.DOMAIN, @@ -160,7 +160,7 @@ async def test_switch_entities( with pytest.raises( HomeAssistantError, - match=r"^An error occurred while communicating with HomeWizard device$", + match=r"^An error occurred while communicating with your HomeWizard Energy device$", ): await hass.services.async_call( switch.DOMAIN, From f9d4bde0f68d252dff226d4b5695c40c22df3349 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 20 Jun 2025 13:44:14 +0200 Subject: [PATCH 52/69] Bump here-routing to 1.2.0 (#147204) * Bump here-routing to 1.2.0 * Fix mypy typing errors * Correct types for call assertion --- homeassistant/components/here_travel_time/coordinator.py | 6 ++++-- homeassistant/components/here_travel_time/manifest.json | 2 +- homeassistant/components/here_travel_time/model.py | 4 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/here_travel_time/test_sensor.py | 4 ++-- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index 2c678316939..d8c698554c9 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -100,9 +100,11 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] try: response = await self._api.route( transport_mode=TransportMode(params.travel_mode), - origin=here_routing.Place(params.origin[0], params.origin[1]), + origin=here_routing.Place( + float(params.origin[0]), float(params.origin[1]) + ), destination=here_routing.Place( - params.destination[0], params.destination[1] + float(params.destination[0]), float(params.destination[1]) ), routing_mode=params.route_mode, arrival_time=params.arrival, diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 0365cf51d97..9d3b622a877 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "iot_class": "cloud_polling", "loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"], - "requirements": ["here-routing==1.0.1", "here-transit==1.2.1"] + "requirements": ["here-routing==1.2.0", "here-transit==1.2.1"] } diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index cbac2b1c353..a0534d2ff01 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from datetime import datetime from typing import TypedDict +from here_routing import RoutingMode + class HERETravelTimeData(TypedDict): """Routing information.""" @@ -27,6 +29,6 @@ class HERETravelTimeAPIParams: destination: list[str] origin: list[str] travel_mode: str - route_mode: str + route_mode: RoutingMode arrival: datetime | None departure: datetime | None diff --git a/requirements_all.txt b/requirements_all.txt index 425d09bd2eb..03dea561c44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ hdate[astral]==1.1.2 heatmiserV3==2.0.3 # homeassistant.components.here_travel_time -here-routing==1.0.1 +here-routing==1.2.0 # homeassistant.components.here_travel_time here-transit==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 924ebc07ef7..3bf2db2ebef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -992,7 +992,7 @@ hassil==2.2.3 hdate[astral]==1.1.2 # homeassistant.components.here_travel_time -here-routing==1.0.1 +here-routing==1.2.0 # homeassistant.components.here_travel_time here-transit==1.2.1 diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 22042f863bc..7c8946b7049 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -319,8 +319,8 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock) -> Non valid_response.assert_called_with( transport_mode=TransportMode.TRUCK, - origin=Place(ORIGIN_LATITUDE, ORIGIN_LONGITUDE), - destination=Place(DESTINATION_LATITUDE, DESTINATION_LONGITUDE), + origin=Place(float(ORIGIN_LATITUDE), float(ORIGIN_LONGITUDE)), + destination=Place(float(DESTINATION_LATITUDE), float(DESTINATION_LONGITUDE)), routing_mode=RoutingMode.FAST, arrival_time=None, departure_time=None, From e28965770eb69bc35519a27bb7632ecddb826484 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 20 Jun 2025 14:31:16 +0200 Subject: [PATCH 53/69] Add translations for devolo Home Control exceptions (#147099) * Add translations for devolo Home Control exceptions * Adapt invalid_auth message * Adapt connection_failed message --- .../components/devolo_home_control/__init__.py | 18 ++++++++++++++---- .../devolo_home_control/strings.json | 11 +++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 331bb5df94a..20a1edf734d 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceEntry -from .const import GATEWAY_SERIAL_PATTERN, PLATFORMS +from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]] @@ -32,10 +32,16 @@ async def async_setup_entry( credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) if not credentials_valid: - raise ConfigEntryAuthFailed + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + ) if await hass.async_add_executor_job(mydevolo.maintenance): - raise ConfigEntryNotReady + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="maintenance", + ) gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids) @@ -69,7 +75,11 @@ async def async_setup_entry( ) ) except GatewayOfflineError as err: - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="connection_failed", + translation_placeholders={"gateway_id": gateway_id}, + ) from err await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/devolo_home_control/strings.json b/homeassistant/components/devolo_home_control/strings.json index be853e2d89d..a5a8086ba47 100644 --- a/homeassistant/components/devolo_home_control/strings.json +++ b/homeassistant/components/devolo_home_control/strings.json @@ -45,5 +45,16 @@ "name": "Brightness" } } + }, + "exceptions": { + "connection_failed": { + "message": "Failed to connect to devolo Home Control central unit {gateway_id}." + }, + "invalid_auth": { + "message": "Authentication failed. Please re-authenticaticate with your mydevolo account." + }, + "maintenance": { + "message": "devolo Home Control is currently in maintenance mode." + } } } From 1b73acc025856f4ae28636d8e6405c7bd934e653 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:52:34 -0400 Subject: [PATCH 54/69] Add sub-device support to Russound RIO (#146763) --- .../components/russound_rio/__init__.py | 35 +++++++++++++++++++ .../components/russound_rio/entity.py | 33 +++++++---------- .../components/russound_rio/media_player.py | 4 +-- tests/components/russound_rio/const.py | 3 +- .../russound_rio/test_media_player.py | 2 +- 5 files changed, 52 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py index 65fbd89e203..f35a476bbb3 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -9,6 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS @@ -52,6 +54,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> ) from err entry.runtime_data = client + device_registry = dr.async_get(hass) + + for controller_id, controller in client.controllers.items(): + _device_identifier = ( + controller.mac_address + or f"{client.controllers[1].mac_address}-{controller_id}" + ) + connections = None + via_device = None + configuration_url = None + if controller_id != 1: + assert client.controllers[1].mac_address + via_device = ( + DOMAIN, + client.controllers[1].mac_address, + ) + else: + assert controller.mac_address + connections = {(CONNECTION_NETWORK_MAC, controller.mac_address)} + if isinstance(client.connection_handler, RussoundTcpConnectionHandler): + configuration_url = f"http://{client.connection_handler.host}" + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, _device_identifier)}, + manufacturer="Russound", + name=controller.controller_type, + model=controller.controller_type, + sw_version=controller.firmware_version, + connections=connections, + via_device=via_device, + configuration_url=configuration_url, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py index 9790ff43e68..d7b4e412831 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -4,11 +4,11 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from typing import Any, Concatenate -from aiorussound import Controller, RussoundClient, RussoundTcpConnectionHandler +from aiorussound import Controller, RussoundClient from aiorussound.models import CallbackType from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS @@ -46,6 +46,7 @@ class RussoundBaseEntity(Entity): def __init__( self, controller: Controller, + zone_id: int | None = None, ) -> None: """Initialize the entity.""" self._client = controller.client @@ -57,29 +58,21 @@ class RussoundBaseEntity(Entity): self._controller.mac_address or f"{self._primary_mac_address}-{self._controller.controller_id}" ) + if not zone_id: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_identifier)}, + ) + return + zone = controller.zones[zone_id] self._attr_device_info = DeviceInfo( - # Use MAC address of Russound device as identifier - identifiers={(DOMAIN, self._device_identifier)}, + identifiers={(DOMAIN, f"{self._device_identifier}-{zone_id}")}, + name=zone.name, manufacturer="Russound", - name=controller.controller_type, model=controller.controller_type, sw_version=controller.firmware_version, + suggested_area=zone.name, + via_device=(DOMAIN, self._device_identifier), ) - if isinstance(self._client.connection_handler, RussoundTcpConnectionHandler): - self._attr_device_info["configuration_url"] = ( - f"http://{self._client.connection_handler.host}" - ) - if controller.controller_id != 1: - assert self._client.controllers[1].mac_address - self._attr_device_info["via_device"] = ( - DOMAIN, - self._client.controllers[1].mac_address, - ) - else: - assert controller.mac_address - self._attr_device_info["connections"] = { - (CONNECTION_NETWORK_MAC, controller.mac_address) - } async def _state_update_callback( self, _client: RussoundClient, _callback_type: CallbackType diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index b40b82862f9..7dbc3ae34be 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -60,16 +60,16 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SEEK ) + _attr_name = None def __init__( self, controller: Controller, zone_id: int, sources: dict[int, Source] ) -> None: """Initialize the zone device.""" - super().__init__(controller) + super().__init__(controller, zone_id) self._zone_id = zone_id _zone = self._zone self._sources = sources - self._attr_name = _zone.name self._attr_unique_id = f"{self._primary_mac_address}-{_zone.device_str}" @property diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 8269e825e33..e801d6786ad 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -17,6 +17,5 @@ MOCK_RECONFIGURATION_CONFIG = { CONF_PORT: 9622, } -DEVICE_NAME = "mca_c5" NAME_ZONE_1 = "backyard" -ENTITY_ID_ZONE_1 = f"{MP_DOMAIN}.{DEVICE_NAME}_{NAME_ZONE_1}" +ENTITY_ID_ZONE_1 = f"{MP_DOMAIN}.{NAME_ZONE_1}" diff --git a/tests/components/russound_rio/test_media_player.py b/tests/components/russound_rio/test_media_player.py index d0c18a9b1e7..04e1057565d 100644 --- a/tests/components/russound_rio/test_media_player.py +++ b/tests/components/russound_rio/test_media_player.py @@ -207,7 +207,7 @@ async def test_invalid_source_service( with pytest.raises( HomeAssistantError, - match="Error executing async_select_source on entity media_player.mca_c5_backyard", + match="Error executing async_select_source on entity media_player.backyard", ): await hass.services.async_call( MP_DOMAIN, From 33bde48c9c53ae05d64f39dcb0c1c7d7cdf22ad6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Jun 2025 08:56:08 -0400 Subject: [PATCH 55/69] AI Task integration (#145128) * Add AI Task integration * Remove GenTextTaskType * Add AI Task prefs * Add action to LLM task * Remove WS command * Rename result to text for GenTextTaskResult * Apply suggestions from code review Co-authored-by: Allen Porter * Add supported feature for generate text * Update const.py Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com> * Update homeassistant/components/ai_task/services.yaml Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com> * Use WS API to set preferences * Simplify pref storage * Simplify pref test * Update homeassistant/components/ai_task/services.yaml Co-authored-by: Allen Porter --------- Co-authored-by: Allen Porter Co-authored-by: HarvsG <11440490+HarvsG@users.noreply.github.com> --- CODEOWNERS | 2 + homeassistant/components/ai_task/__init__.py | 125 +++++++++++++++++ homeassistant/components/ai_task/const.py | 29 ++++ homeassistant/components/ai_task/entity.py | 103 ++++++++++++++ homeassistant/components/ai_task/http.py | 54 ++++++++ homeassistant/components/ai_task/icons.json | 7 + .../components/ai_task/manifest.json | 9 ++ .../components/ai_task/services.yaml | 19 +++ homeassistant/components/ai_task/strings.json | 22 +++ homeassistant/components/ai_task/task.py | 71 ++++++++++ homeassistant/const.py | 1 + tests/components/ai_task/__init__.py | 1 + tests/components/ai_task/conftest.py | 127 ++++++++++++++++++ .../ai_task/snapshots/test_task.ambr | 22 +++ tests/components/ai_task/test_entity.py | 39 ++++++ tests/components/ai_task/test_http.py | 84 ++++++++++++ tests/components/ai_task/test_init.py | 84 ++++++++++++ tests/components/ai_task/test_task.py | 123 +++++++++++++++++ 18 files changed, 922 insertions(+) create mode 100644 homeassistant/components/ai_task/__init__.py create mode 100644 homeassistant/components/ai_task/const.py create mode 100644 homeassistant/components/ai_task/entity.py create mode 100644 homeassistant/components/ai_task/http.py create mode 100644 homeassistant/components/ai_task/icons.json create mode 100644 homeassistant/components/ai_task/manifest.json create mode 100644 homeassistant/components/ai_task/services.yaml create mode 100644 homeassistant/components/ai_task/strings.json create mode 100644 homeassistant/components/ai_task/task.py create mode 100644 tests/components/ai_task/__init__.py create mode 100644 tests/components/ai_task/conftest.py create mode 100644 tests/components/ai_task/snapshots/test_task.ambr create mode 100644 tests/components/ai_task/test_entity.py create mode 100644 tests/components/ai_task/test_http.py create mode 100644 tests/components/ai_task/test_init.py create mode 100644 tests/components/ai_task/test_task.py diff --git a/CODEOWNERS b/CODEOWNERS index 6670b411df4..1ceb6ff0e7d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -57,6 +57,8 @@ build.json @home-assistant/supervisor /tests/components/aemet/ @Noltari /homeassistant/components/agent_dvr/ @ispysoftware /tests/components/agent_dvr/ @ispysoftware +/homeassistant/components/ai_task/ @home-assistant/core +/tests/components/ai_task/ @home-assistant/core /homeassistant/components/air_quality/ @home-assistant/core /tests/components/air_quality/ @home-assistant/core /homeassistant/components/airgradient/ @airgradienthq @joostlek diff --git a/homeassistant/components/ai_task/__init__.py b/homeassistant/components/ai_task/__init__.py new file mode 100644 index 00000000000..8b3d6e04966 --- /dev/null +++ b/homeassistant/components/ai_task/__init__.py @@ -0,0 +1,125 @@ +"""Integration to offer AI tasks to Home Assistant.""" + +import logging + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import ( + HassJobType, + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) +from homeassistant.helpers import config_validation as cv, storage +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType + +from .const import DATA_COMPONENT, DATA_PREFERENCES, DOMAIN, AITaskEntityFeature +from .entity import AITaskEntity +from .http import async_setup as async_setup_conversation_http +from .task import GenTextTask, GenTextTaskResult, async_generate_text + +__all__ = [ + "DOMAIN", + "AITaskEntity", + "AITaskEntityFeature", + "GenTextTask", + "GenTextTaskResult", + "async_generate_text", + "async_setup", + "async_setup_entry", + "async_unload_entry", +] + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Register the process service.""" + entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass) + hass.data[DATA_COMPONENT] = entity_component + hass.data[DATA_PREFERENCES] = AITaskPreferences(hass) + await hass.data[DATA_PREFERENCES].async_load() + async_setup_conversation_http(hass) + hass.services.async_register( + DOMAIN, + "generate_text", + async_service_generate_text, + schema=vol.Schema( + { + vol.Required("task_name"): cv.string, + vol.Optional("entity_id"): cv.entity_id, + vol.Required("instructions"): cv.string, + } + ), + supports_response=SupportsResponse.ONLY, + job_type=HassJobType.Coroutinefunction, + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + return await hass.data[DATA_COMPONENT].async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.data[DATA_COMPONENT].async_unload_entry(entry) + + +async def async_service_generate_text(call: ServiceCall) -> ServiceResponse: + """Run the run task service.""" + result = await async_generate_text(hass=call.hass, **call.data) + return result.as_dict() # type: ignore[return-value] + + +class AITaskPreferences: + """AI Task preferences.""" + + KEYS = ("gen_text_entity_id",) + + gen_text_entity_id: str | None = None + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the preferences.""" + self._store: storage.Store[dict[str, str | None]] = storage.Store( + hass, 1, DOMAIN + ) + + async def async_load(self) -> None: + """Load the data from the store.""" + data = await self._store.async_load() + if data is None: + return + for key in self.KEYS: + setattr(self, key, data[key]) + + @callback + def async_set_preferences( + self, + *, + gen_text_entity_id: str | None | UndefinedType = UNDEFINED, + ) -> None: + """Set the preferences.""" + changed = False + for key, value in (("gen_text_entity_id", gen_text_entity_id),): + if value is not UNDEFINED: + if getattr(self, key) != value: + setattr(self, key, value) + changed = True + + if not changed: + return + + self._store.async_delay_save(self.as_dict, 10) + + @callback + def as_dict(self) -> dict[str, str | None]: + """Get the current preferences.""" + return {key: getattr(self, key) for key in self.KEYS} diff --git a/homeassistant/components/ai_task/const.py b/homeassistant/components/ai_task/const.py new file mode 100644 index 00000000000..69786178583 --- /dev/null +++ b/homeassistant/components/ai_task/const.py @@ -0,0 +1,29 @@ +"""Constants for the AI Task integration.""" + +from __future__ import annotations + +from enum import IntFlag +from typing import TYPE_CHECKING + +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from homeassistant.helpers.entity_component import EntityComponent + + from . import AITaskPreferences + from .entity import AITaskEntity + +DOMAIN = "ai_task" +DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN) +DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences") + +DEFAULT_SYSTEM_PROMPT = ( + "You are a Home Assistant expert and help users with their tasks." +) + + +class AITaskEntityFeature(IntFlag): + """Supported features of the AI task entity.""" + + GENERATE_TEXT = 1 + """Generate text based on instructions.""" diff --git a/homeassistant/components/ai_task/entity.py b/homeassistant/components/ai_task/entity.py new file mode 100644 index 00000000000..88ce8144fb7 --- /dev/null +++ b/homeassistant/components/ai_task/entity.py @@ -0,0 +1,103 @@ +"""Entity for the AI Task integration.""" + +from collections.abc import AsyncGenerator +import contextlib +from typing import final + +from propcache.api import cached_property + +from homeassistant.components.conversation import ( + ChatLog, + UserContent, + async_get_chat_log, +) +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.helpers import llm +from homeassistant.helpers.chat_session import async_get_chat_session +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import dt as dt_util + +from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature +from .task import GenTextTask, GenTextTaskResult + + +class AITaskEntity(RestoreEntity): + """Entity that supports conversations.""" + + _attr_should_poll = False + _attr_supported_features = AITaskEntityFeature(0) + __last_activity: str | None = None + + @property + @final + def state(self) -> str | None: + """Return the state of the entity.""" + if self.__last_activity is None: + return None + return self.__last_activity + + @cached_property + def supported_features(self) -> AITaskEntityFeature: + """Flag supported features.""" + return self._attr_supported_features + + async def async_internal_added_to_hass(self) -> None: + """Call when the entity 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 + and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) + ): + self.__last_activity = state.state + + @final + @contextlib.asynccontextmanager + async def _async_get_ai_task_chat_log( + self, + task: GenTextTask, + ) -> AsyncGenerator[ChatLog]: + """Context manager used to manage the ChatLog used during an AI Task.""" + # pylint: disable-next=contextmanager-generator-missing-cleanup + with ( + async_get_chat_session(self.hass) as session, + async_get_chat_log( + self.hass, + session, + None, + ) as chat_log, + ): + await chat_log.async_provide_llm_data( + llm.LLMContext( + platform=self.platform.domain, + context=None, + language=None, + assistant=DOMAIN, + device_id=None, + ), + user_llm_prompt=DEFAULT_SYSTEM_PROMPT, + ) + + chat_log.async_add_user_content(UserContent(task.instructions)) + + yield chat_log + + @final + async def internal_async_generate_text( + self, + task: GenTextTask, + ) -> GenTextTaskResult: + """Run a gen text task.""" + self.__last_activity = dt_util.utcnow().isoformat() + self.async_write_ha_state() + async with self._async_get_ai_task_chat_log(task) as chat_log: + return await self._async_generate_text(task, chat_log) + + async def _async_generate_text( + self, + task: GenTextTask, + chat_log: ChatLog, + ) -> GenTextTaskResult: + """Handle a gen text task.""" + raise NotImplementedError diff --git a/homeassistant/components/ai_task/http.py b/homeassistant/components/ai_task/http.py new file mode 100644 index 00000000000..6d44a4e8d3c --- /dev/null +++ b/homeassistant/components/ai_task/http.py @@ -0,0 +1,54 @@ +"""HTTP endpoint for AI Task integration.""" + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DATA_PREFERENCES + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the HTTP API for the conversation integration.""" + websocket_api.async_register_command(hass, websocket_get_preferences) + websocket_api.async_register_command(hass, websocket_set_preferences) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "ai_task/preferences/get", + } +) +@callback +def websocket_get_preferences( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get AI task preferences.""" + preferences = hass.data[DATA_PREFERENCES] + connection.send_result(msg["id"], preferences.as_dict()) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "ai_task/preferences/set", + vol.Optional("gen_text_entity_id"): vol.Any(str, None), + } +) +@websocket_api.require_admin +@callback +def websocket_set_preferences( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Set AI task preferences.""" + preferences = hass.data[DATA_PREFERENCES] + msg.pop("type") + msg_id = msg.pop("id") + preferences.async_set_preferences(**msg) + connection.send_result(msg_id, preferences.as_dict()) diff --git a/homeassistant/components/ai_task/icons.json b/homeassistant/components/ai_task/icons.json new file mode 100644 index 00000000000..cb09e5c8f5d --- /dev/null +++ b/homeassistant/components/ai_task/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "generate_text": { + "service": "mdi:file-star-four-points-outline" + } + } +} diff --git a/homeassistant/components/ai_task/manifest.json b/homeassistant/components/ai_task/manifest.json new file mode 100644 index 00000000000..c685410530d --- /dev/null +++ b/homeassistant/components/ai_task/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ai_task", + "name": "AI Task", + "codeowners": ["@home-assistant/core"], + "dependencies": ["conversation"], + "documentation": "https://www.home-assistant.io/integrations/ai_task", + "integration_type": "system", + "quality_scale": "internal" +} diff --git a/homeassistant/components/ai_task/services.yaml b/homeassistant/components/ai_task/services.yaml new file mode 100644 index 00000000000..32715bf77d7 --- /dev/null +++ b/homeassistant/components/ai_task/services.yaml @@ -0,0 +1,19 @@ +generate_text: + fields: + task_name: + example: "home summary" + required: true + selector: + text: + instructions: + example: "Generate a funny notification that garage door was left open" + required: true + selector: + text: + entity_id: + required: false + selector: + entity: + domain: ai_task + supported_features: + - ai_task.AITaskEntityFeature.GENERATE_TEXT diff --git a/homeassistant/components/ai_task/strings.json b/homeassistant/components/ai_task/strings.json new file mode 100644 index 00000000000..1cdbf20ba4f --- /dev/null +++ b/homeassistant/components/ai_task/strings.json @@ -0,0 +1,22 @@ +{ + "services": { + "generate_text": { + "name": "Generate text", + "description": "Use AI to run a task that generates text.", + "fields": { + "task_name": { + "name": "Task Name", + "description": "Name of the task." + }, + "instructions": { + "name": "Instructions", + "description": "Instructions on what needs to be done." + }, + "entity_id": { + "name": "Entity ID", + "description": "Entity ID to run the task on. If not provided, the preferred entity will be used." + } + } + } + } +} diff --git a/homeassistant/components/ai_task/task.py b/homeassistant/components/ai_task/task.py new file mode 100644 index 00000000000..d0c59fdd09a --- /dev/null +++ b/homeassistant/components/ai_task/task.py @@ -0,0 +1,71 @@ +"""AI tasks to be handled by agents.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.core import HomeAssistant + +from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature + + +async def async_generate_text( + hass: HomeAssistant, + *, + task_name: str, + entity_id: str | None = None, + instructions: str, +) -> GenTextTaskResult: + """Run a task in the AI Task integration.""" + if entity_id is None: + entity_id = hass.data[DATA_PREFERENCES].gen_text_entity_id + + if entity_id is None: + raise ValueError("No entity_id provided and no preferred entity set") + + entity = hass.data[DATA_COMPONENT].get_entity(entity_id) + if entity is None: + raise ValueError(f"AI Task entity {entity_id} not found") + + if AITaskEntityFeature.GENERATE_TEXT not in entity.supported_features: + raise ValueError(f"AI Task entity {entity_id} does not support generating text") + + return await entity.internal_async_generate_text( + GenTextTask( + name=task_name, + instructions=instructions, + ) + ) + + +@dataclass(slots=True) +class GenTextTask: + """Gen text task to be processed.""" + + name: str + """Name of the task.""" + + instructions: str + """Instructions on what needs to be done.""" + + def __str__(self) -> str: + """Return task as a string.""" + return f"" + + +@dataclass(slots=True) +class GenTextTaskResult: + """Result of gen text task.""" + + conversation_id: str + """Unique identifier for the conversation.""" + + text: str + """Generated text.""" + + def as_dict(self) -> dict[str, str]: + """Return result as a dict.""" + return { + "conversation_id": self.conversation_id, + "text": self.text, + } diff --git a/homeassistant/const.py b/homeassistant/const.py index f692f428920..0abdcd59b77 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -40,6 +40,7 @@ PLATFORM_FORMAT: Final = "{platform}.{domain}" class Platform(StrEnum): """Available entity platforms.""" + AI_TASK = "ai_task" AIR_QUALITY = "air_quality" ALARM_CONTROL_PANEL = "alarm_control_panel" ASSIST_SATELLITE = "assist_satellite" diff --git a/tests/components/ai_task/__init__.py b/tests/components/ai_task/__init__.py new file mode 100644 index 00000000000..b4ca4688eb4 --- /dev/null +++ b/tests/components/ai_task/__init__.py @@ -0,0 +1 @@ +"""Tests for the AI Task integration.""" diff --git a/tests/components/ai_task/conftest.py b/tests/components/ai_task/conftest.py new file mode 100644 index 00000000000..2060c51bfa4 --- /dev/null +++ b/tests/components/ai_task/conftest.py @@ -0,0 +1,127 @@ +"""Test helpers for AI Task integration.""" + +import pytest + +from homeassistant.components.ai_task import ( + DOMAIN, + AITaskEntity, + AITaskEntityFeature, + GenTextTask, + GenTextTaskResult, +) +from homeassistant.components.conversation import AssistantContent, ChatLog +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" +TEST_ENTITY_ID = "ai_task.test_task_entity" + + +class MockAITaskEntity(AITaskEntity): + """Mock AI Task entity for testing.""" + + _attr_name = "Test Task Entity" + _attr_supported_features = AITaskEntityFeature.GENERATE_TEXT + + def __init__(self) -> None: + """Initialize the mock entity.""" + super().__init__() + self.mock_generate_text_tasks = [] + + async def _async_generate_text( + self, task: GenTextTask, chat_log: ChatLog + ) -> GenTextTaskResult: + """Mock handling of generate text task.""" + self.mock_generate_text_tasks.append(task) + chat_log.async_add_assistant_content_without_tools( + AssistantContent(self.entity_id, "Mock result") + ) + return GenTextTaskResult( + conversation_id=chat_log.conversation_id, + text="Mock result", + ) + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Mock a configuration entry for AI Task.""" + entry = MockConfigEntry(domain=TEST_DOMAIN, entry_id="mock-test-entry") + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +def mock_ai_task_entity( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockAITaskEntity: + """Mock AI Task entity.""" + return MockAITaskEntity() + + +@pytest.fixture +async def init_components( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ai_task_entity: MockAITaskEntity, +): + """Initialize the AI Task integration with a mock entity.""" + assert await async_setup_component(hass, "homeassistant", {}) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.AI_TASK] + ) + return True + + async def async_unload_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload test config entry.""" + await hass.config_entries.async_forward_entry_unload( + config_entry, Platform.AI_TASK + ) + return True + + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + async_unload_entry=async_unload_entry_init, + ), + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, + ) -> None: + """Set up test tts platform via config entry.""" + async_add_entities([mock_ai_task_entity]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, ConfigFlow): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/ai_task/snapshots/test_task.ambr b/tests/components/ai_task/snapshots/test_task.ambr new file mode 100644 index 00000000000..6d155c82a68 --- /dev/null +++ b/tests/components/ai_task/snapshots/test_task.ambr @@ -0,0 +1,22 @@ +# serializer version: 1 +# name: test_run_text_task_updates_chat_log + list([ + dict({ + 'content': ''' + You are a Home Assistant expert and help users with their tasks. + Current time is 15:59:00. Today's date is 2025-06-14. + ''', + 'role': 'system', + }), + dict({ + 'content': 'Test prompt', + 'role': 'user', + }), + dict({ + 'agent_id': 'ai_task.test_task_entity', + 'content': 'Mock result', + 'role': 'assistant', + 'tool_calls': None, + }), + ]) +# --- diff --git a/tests/components/ai_task/test_entity.py b/tests/components/ai_task/test_entity.py new file mode 100644 index 00000000000..aa9afbf6560 --- /dev/null +++ b/tests/components/ai_task/test_entity.py @@ -0,0 +1,39 @@ +"""Tests for the AI Task entity model.""" + +from freezegun import freeze_time + +from homeassistant.components.ai_task import async_generate_text +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .conftest import TEST_ENTITY_ID, MockAITaskEntity + +from tests.common import MockConfigEntry + + +@freeze_time("2025-06-08 16:28:13") +async def test_state_generate_text( + hass: HomeAssistant, + init_components: None, + mock_config_entry: MockConfigEntry, + mock_ai_task_entity: MockAITaskEntity, +) -> None: + """Test the state of the AI Task entity is updated when generating text.""" + entity = hass.states.get(TEST_ENTITY_ID) + assert entity is not None + assert entity.state == STATE_UNKNOWN + + result = await async_generate_text( + hass, + task_name="Test task", + entity_id=TEST_ENTITY_ID, + instructions="Test prompt", + ) + assert result.text == "Mock result" + + entity = hass.states.get(TEST_ENTITY_ID) + assert entity.state == "2025-06-08T16:28:13+00:00" + + assert mock_ai_task_entity.mock_generate_text_tasks + task = mock_ai_task_entity.mock_generate_text_tasks[0] + assert task.instructions == "Test prompt" diff --git a/tests/components/ai_task/test_http.py b/tests/components/ai_task/test_http.py new file mode 100644 index 00000000000..4436e1d45d5 --- /dev/null +++ b/tests/components/ai_task/test_http.py @@ -0,0 +1,84 @@ +"""Test the HTTP API for AI Task integration.""" + +from homeassistant.core import HomeAssistant + +from tests.typing import WebSocketGenerator + + +async def test_ws_preferences( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components: None, +) -> None: + """Test preferences via the WebSocket API.""" + client = await hass_ws_client(hass) + + # Get initial preferences + await client.send_json_auto_id({"type": "ai_task/preferences/get"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": None, + } + + # Set preferences + await client.send_json_auto_id( + { + "type": "ai_task/preferences/set", + "gen_text_entity_id": "ai_task.summary_1", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": "ai_task.summary_1", + } + + # Get updated preferences + await client.send_json_auto_id({"type": "ai_task/preferences/get"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": "ai_task.summary_1", + } + + # Update an existing preference + await client.send_json_auto_id( + { + "type": "ai_task/preferences/set", + "gen_text_entity_id": "ai_task.summary_2", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": "ai_task.summary_2", + } + + # Get updated preferences + await client.send_json_auto_id({"type": "ai_task/preferences/get"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": "ai_task.summary_2", + } + + # No preferences set will preserve existing preferences + await client.send_json_auto_id( + { + "type": "ai_task/preferences/set", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": "ai_task.summary_2", + } + + # Get updated preferences + await client.send_json_auto_id({"type": "ai_task/preferences/get"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "gen_text_entity_id": "ai_task.summary_2", + } diff --git a/tests/components/ai_task/test_init.py b/tests/components/ai_task/test_init.py new file mode 100644 index 00000000000..2f45d812b1f --- /dev/null +++ b/tests/components/ai_task/test_init.py @@ -0,0 +1,84 @@ +"""Test initialization of the AI Task component.""" + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.ai_task import AITaskPreferences +from homeassistant.components.ai_task.const import DATA_PREFERENCES +from homeassistant.core import HomeAssistant + +from .conftest import TEST_ENTITY_ID + +from tests.common import flush_store + + +async def test_preferences_storage_load( + hass: HomeAssistant, +) -> None: + """Test that AITaskPreferences are stored and loaded correctly.""" + preferences = AITaskPreferences(hass) + await preferences.async_load() + + # Initial state should be None for entity IDs + for key in AITaskPreferences.KEYS: + assert getattr(preferences, key) is None, f"Initial {key} should be None" + + new_values = {key: f"ai_task.test_{key}" for key in AITaskPreferences.KEYS} + + preferences.async_set_preferences(**new_values) + + # Verify that current preferences object is updated + for key, value in new_values.items(): + assert getattr(preferences, key) == value, ( + f"Current {key} should match set value" + ) + + await flush_store(preferences._store) + + # Create a new preferences instance to test loading from store + new_preferences_instance = AITaskPreferences(hass) + await new_preferences_instance.async_load() + + for key in AITaskPreferences.KEYS: + assert getattr(preferences, key) == getattr(new_preferences_instance, key), ( + f"Loaded {key} should match saved value" + ) + + +@pytest.mark.parametrize( + ("set_preferences", "msg_extra"), + [ + ( + {"gen_text_entity_id": TEST_ENTITY_ID}, + {}, + ), + ( + {}, + {"entity_id": TEST_ENTITY_ID}, + ), + ], +) +async def test_generate_text_service( + hass: HomeAssistant, + init_components: None, + freezer: FrozenDateTimeFactory, + set_preferences: dict[str, str | None], + msg_extra: dict[str, str], +) -> None: + """Test the generate text service.""" + preferences = hass.data[DATA_PREFERENCES] + preferences.async_set_preferences(**set_preferences) + + result = await hass.services.async_call( + "ai_task", + "generate_text", + { + "task_name": "Test Name", + "instructions": "Test prompt", + } + | msg_extra, + blocking=True, + return_response=True, + ) + + assert result["text"] == "Mock result" diff --git a/tests/components/ai_task/test_task.py b/tests/components/ai_task/test_task.py new file mode 100644 index 00000000000..d4df66d83f9 --- /dev/null +++ b/tests/components/ai_task/test_task.py @@ -0,0 +1,123 @@ +"""Test tasks for the AI Task integration.""" + +from freezegun import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.ai_task import AITaskEntityFeature, async_generate_text +from homeassistant.components.conversation import async_get_chat_log +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import chat_session + +from .conftest import TEST_ENTITY_ID, MockAITaskEntity + +from tests.typing import WebSocketGenerator + + +async def test_run_task_preferred_entity( + hass: HomeAssistant, + init_components: None, + mock_ai_task_entity: MockAITaskEntity, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test running a task with an unknown entity.""" + client = await hass_ws_client(hass) + + with pytest.raises( + ValueError, match="No entity_id provided and no preferred entity set" + ): + await async_generate_text( + hass, + task_name="Test Task", + instructions="Test prompt", + ) + + await client.send_json_auto_id( + { + "type": "ai_task/preferences/set", + "gen_text_entity_id": "ai_task.unknown", + } + ) + msg = await client.receive_json() + assert msg["success"] + + with pytest.raises(ValueError, match="AI Task entity ai_task.unknown not found"): + await async_generate_text( + hass, + task_name="Test Task", + instructions="Test prompt", + ) + + await client.send_json_auto_id( + { + "type": "ai_task/preferences/set", + "gen_text_entity_id": TEST_ENTITY_ID, + } + ) + msg = await client.receive_json() + assert msg["success"] + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_UNKNOWN + + result = await async_generate_text( + hass, + task_name="Test Task", + instructions="Test prompt", + ) + assert result.text == "Mock result" + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state != STATE_UNKNOWN + + mock_ai_task_entity.supported_features = AITaskEntityFeature(0) + with pytest.raises( + ValueError, + match="AI Task entity ai_task.test_task_entity does not support generating text", + ): + await async_generate_text( + hass, + task_name="Test Task", + instructions="Test prompt", + ) + + +async def test_run_text_task_unknown_entity( + hass: HomeAssistant, + init_components: None, +) -> None: + """Test running a text task with an unknown entity.""" + + with pytest.raises( + ValueError, match="AI Task entity ai_task.unknown_entity not found" + ): + await async_generate_text( + hass, + task_name="Test Task", + entity_id="ai_task.unknown_entity", + instructions="Test prompt", + ) + + +@freeze_time("2025-06-14 22:59:00") +async def test_run_text_task_updates_chat_log( + hass: HomeAssistant, + init_components: None, + snapshot: SnapshotAssertion, +) -> None: + """Test that running a text task updates the chat log.""" + result = await async_generate_text( + hass, + task_name="Test Task", + entity_id=TEST_ENTITY_ID, + instructions="Test prompt", + ) + assert result.text == "Mock result" + + with ( + chat_session.async_get_chat_session(hass, result.conversation_id) as session, + async_get_chat_log(hass, session) as chat_log, + ): + assert chat_log.content == snapshot From 46aea5d9dce0291948bee81a48b1e4e426117510 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Fri, 20 Jun 2025 15:59:54 +0300 Subject: [PATCH 56/69] Bump zwave-js-server-python to 0.64.0 (#147176) --- homeassistant/components/zwave_js/api.py | 45 +++++++++---------- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 22 ++++----- 5 files changed, 35 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index c1a24b6ea65..168df5edcaa 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -32,19 +32,19 @@ from zwave_js_server.exceptions import ( NotFoundError, SetValueFailed, ) -from zwave_js_server.firmware import controller_firmware_update_otw, update_firmware +from zwave_js_server.firmware import driver_firmware_update_otw, update_firmware from zwave_js_server.model.controller import ( ControllerStatistics, InclusionGrant, ProvisioningEntry, QRProvisioningInformation, ) -from zwave_js_server.model.controller.firmware import ( - ControllerFirmwareUpdateData, - ControllerFirmwareUpdateProgress, - ControllerFirmwareUpdateResult, -) from zwave_js_server.model.driver import Driver +from zwave_js_server.model.driver.firmware import ( + DriverFirmwareUpdateData, + DriverFirmwareUpdateProgress, + DriverFirmwareUpdateResult, +) from zwave_js_server.model.endpoint import Endpoint from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage @@ -2340,8 +2340,8 @@ def _get_node_firmware_update_progress_dict( } -def _get_controller_firmware_update_progress_dict( - progress: ControllerFirmwareUpdateProgress, +def _get_driver_firmware_update_progress_dict( + progress: DriverFirmwareUpdateProgress, ) -> dict[str, int | float]: """Get a dictionary of a controller's firmware update progress.""" return { @@ -2370,7 +2370,8 @@ async def websocket_subscribe_firmware_update_status( ) -> None: """Subscribe to the status of a firmware update.""" assert node.client.driver - controller = node.client.driver.controller + driver = node.client.driver + controller = driver.controller @callback def async_cleanup() -> None: @@ -2408,21 +2409,21 @@ async def websocket_subscribe_firmware_update_status( ) @callback - def forward_controller_progress(event: dict) -> None: - progress: ControllerFirmwareUpdateProgress = event["firmware_update_progress"] + def forward_driver_progress(event: dict) -> None: + progress: DriverFirmwareUpdateProgress = event["firmware_update_progress"] connection.send_message( websocket_api.event_message( msg[ID], { "event": event["event"], - **_get_controller_firmware_update_progress_dict(progress), + **_get_driver_firmware_update_progress_dict(progress), }, ) ) @callback - def forward_controller_finished(event: dict) -> None: - finished: ControllerFirmwareUpdateResult = event["firmware_update_finished"] + def forward_driver_finished(event: dict) -> None: + finished: DriverFirmwareUpdateResult = event["firmware_update_finished"] connection.send_message( websocket_api.event_message( msg[ID], @@ -2436,8 +2437,8 @@ async def websocket_subscribe_firmware_update_status( if controller.own_node == node: msg[DATA_UNSUBSCRIBE] = unsubs = [ - controller.on("firmware update progress", forward_controller_progress), - controller.on("firmware update finished", forward_controller_finished), + driver.on("firmware update progress", forward_driver_progress), + driver.on("firmware update finished", forward_driver_finished), ] else: msg[DATA_UNSUBSCRIBE] = unsubs = [ @@ -2447,17 +2448,13 @@ async def websocket_subscribe_firmware_update_status( connection.subscriptions[msg["id"]] = async_cleanup connection.send_result(msg[ID]) - if node.is_controller_node and ( - controller_progress := controller.firmware_update_progress - ): + if node.is_controller_node and (driver_progress := driver.firmware_update_progress): connection.send_message( websocket_api.event_message( msg[ID], { "event": "firmware update progress", - **_get_controller_firmware_update_progress_dict( - controller_progress - ), + **_get_driver_firmware_update_progress_dict(driver_progress), }, ) ) @@ -2559,9 +2556,9 @@ class FirmwareUploadView(HomeAssistantView): try: if node.client.driver.controller.own_node == node: - await controller_firmware_update_otw( + await driver_firmware_update_otw( node.client.ws_server_url, - ControllerFirmwareUpdateData( + DriverFirmwareUpdateData( uploaded_file.filename, await hass.async_add_executor_job(uploaded_file.file.read), ), diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 8719c333753..082a3dd9f95 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["zwave_js_server"], - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.63.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.64.0"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index 03dea561c44..90e902ed8c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3193,7 +3193,7 @@ ziggo-mediabox-xl==1.1.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.63.0 +zwave-js-server-python==0.64.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf2db2ebef..633d6904fc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2625,7 +2625,7 @@ zeversolar==0.3.2 zha==0.0.60 # homeassistant.components.zwave_js -zwave-js-server-python==0.63.0 +zwave-js-server-python==0.64.0 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 83a22cbee32..3f1f9b737bd 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -32,7 +32,7 @@ from zwave_js_server.model.controller import ( ProvisioningEntry, QRProvisioningInformation, ) -from zwave_js_server.model.controller.firmware import ControllerFirmwareUpdateData +from zwave_js_server.model.driver.firmware import DriverFirmwareUpdateData from zwave_js_server.model.node import Node from zwave_js_server.model.node.firmware import NodeFirmwareUpdateData from zwave_js_server.model.value import ConfigurationValue, get_value_id_str @@ -3501,7 +3501,7 @@ async def test_firmware_upload_view( "homeassistant.components.zwave_js.api.update_firmware", ) as mock_node_cmd, patch( - "homeassistant.components.zwave_js.api.controller_firmware_update_otw", + "homeassistant.components.zwave_js.api.driver_firmware_update_otw", ) as mock_controller_cmd, patch.dict( "homeassistant.components.zwave_js.api.USER_AGENT", @@ -3544,7 +3544,7 @@ async def test_firmware_upload_view_controller( "homeassistant.components.zwave_js.api.update_firmware", ) as mock_node_cmd, patch( - "homeassistant.components.zwave_js.api.controller_firmware_update_otw", + "homeassistant.components.zwave_js.api.driver_firmware_update_otw", ) as mock_controller_cmd, patch.dict( "homeassistant.components.zwave_js.api.USER_AGENT", @@ -3557,7 +3557,7 @@ async def test_firmware_upload_view_controller( ) mock_node_cmd.assert_not_called() assert mock_controller_cmd.call_args[0][1:2] == ( - ControllerFirmwareUpdateData( + DriverFirmwareUpdateData( "file", b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ), ) @@ -4415,7 +4415,7 @@ async def test_subscribe_controller_firmware_update_status( event = Event( type="firmware update progress", data={ - "source": "controller", + "source": "driver", "event": "firmware update progress", "progress": { "sentFragments": 1, @@ -4424,7 +4424,7 @@ async def test_subscribe_controller_firmware_update_status( }, }, ) - client.driver.controller.receive_event(event) + client.driver.receive_event(event) msg = await ws_client.receive_json() assert msg["event"] == { @@ -4439,7 +4439,7 @@ async def test_subscribe_controller_firmware_update_status( event = Event( type="firmware update finished", data={ - "source": "controller", + "source": "driver", "event": "firmware update finished", "result": { "status": 255, @@ -4447,7 +4447,7 @@ async def test_subscribe_controller_firmware_update_status( }, }, ) - client.driver.controller.receive_event(event) + client.driver.receive_event(event) msg = await ws_client.receive_json() assert msg["event"] == { @@ -4464,13 +4464,13 @@ async def test_subscribe_controller_firmware_update_status_initial_value( ws_client = await hass_ws_client(hass) device = get_device(hass, client.driver.controller.nodes[1]) - assert client.driver.controller.firmware_update_progress is None + assert client.driver.firmware_update_progress is None # Send a firmware update progress event before the WS command event = Event( type="firmware update progress", data={ - "source": "controller", + "source": "driver", "event": "firmware update progress", "progress": { "sentFragments": 1, @@ -4479,7 +4479,7 @@ async def test_subscribe_controller_firmware_update_status_initial_value( }, }, ) - client.driver.controller.receive_event(event) + client.driver.receive_event(event) client.async_send_command_no_wait.return_value = {} From f7429f343154cdf04a3fadc65284528372ed861b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 20 Jun 2025 15:19:39 +0200 Subject: [PATCH 57/69] Fix Shelly entity names for gen1 sleeping devices (#147019) --- homeassistant/components/shelly/entity.py | 1 - tests/components/shelly/test_sensor.py | 44 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 2c1678d56d9..5a420a4543b 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -653,7 +653,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): ) elif entry is not None: self._attr_unique_id = entry.unique_id - self._attr_name = cast(str, entry.original_name) @callback def _update_callback(self) -> None: diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index e95d4cfaeb2..8f021c2d58a 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -23,6 +23,7 @@ from homeassistant.components.shelly.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, @@ -40,6 +41,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from . import ( + MOCK_MAC, init_integration, mock_polling_rpc_update, mock_rest_update, @@ -1585,3 +1587,45 @@ async def test_rpc_switch_no_returned_energy_sensor( await init_integration(hass, 3) assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None + + +async def test_block_friendly_name_sleeping_sensor( + hass: HomeAssistant, + mock_block_device: Mock, + device_registry: DeviceRegistry, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test friendly name for restored sleeping sensor.""" + entry = await init_integration(hass, 1, sleep_period=1000, skip_setup=True) + device = register_device(device_registry, entry) + + entity = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"{MOCK_MAC}-sensor_0-temp", + suggested_object_id="test_name_temperature", + original_name="Test name temperature", + disabled_by=None, + config_entry=entry, + device_id=device.id, + ) + + # Old name, the word "temperature" starts with a lower case letter + assert entity.original_name == "Test name temperature" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity.entity_id)) + + # New name, the word "temperature" starts with a capital letter + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature" + + # Make device online + monkeypatch.setattr(mock_block_device, "initialized", True) + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert (state := hass.states.get(entity.entity_id)) + assert state.attributes[ATTR_FRIENDLY_NAME] == "Test name Temperature" From d9e5bad55e810a69b13c0cdcd9d9b6f744cfe687 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Fri, 20 Jun 2025 16:55:48 +0200 Subject: [PATCH 58/69] Use entity name in homee (#147142) * add name to HomeeEntity * review change --- homeassistant/components/homee/entity.py | 2 ++ tests/components/homee/fixtures/events.json | 2 +- tests/components/homee/snapshots/test_event.ambr | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homee/entity.py b/homeassistant/components/homee/entity.py index d8344c4226a..ddb16315e7d 100644 --- a/homeassistant/components/homee/entity.py +++ b/homeassistant/components/homee/entity.py @@ -42,6 +42,8 @@ class HomeeEntity(Entity): model=get_name_for_enum(NodeProfile, node.profile), via_device=(DOMAIN, entry.runtime_data.settings.uid), ) + if attribute.name: + self._attr_name = attribute.name self._host_connected = entry.runtime_data.connected diff --git a/tests/components/homee/fixtures/events.json b/tests/components/homee/fixtures/events.json index dc541bca597..f1d5c961ce9 100644 --- a/tests/components/homee/fixtures/events.json +++ b/tests/components/homee/fixtures/events.json @@ -61,7 +61,7 @@ "changed_by_id": 0, "based_on": 1, "data": "", - "name": "" + "name": "Kitchen Light" }, { "id": 3, diff --git a/tests/components/homee/snapshots/test_event.ambr b/tests/components/homee/snapshots/test_event.ambr index 40b9a99fcc4..981b6263984 100644 --- a/tests/components/homee/snapshots/test_event.ambr +++ b/tests/components/homee/snapshots/test_event.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_event_snapshot[event.remote_control_switch_1-entry] +# name: test_event_snapshot[event.remote_control_kitchen_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -18,7 +18,7 @@ 'disabled_by': None, 'domain': 'event', 'entity_category': None, - 'entity_id': 'event.remote_control_switch_1', + 'entity_id': 'event.remote_control_kitchen_light', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -30,7 +30,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Switch 1', + 'original_name': 'Kitchen Light', 'platform': 'homee', 'previous_unique_id': None, 'suggested_object_id': None, @@ -40,7 +40,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_event_snapshot[event.remote_control_switch_1-state] +# name: test_event_snapshot[event.remote_control_kitchen_light-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'button', @@ -50,10 +50,10 @@ 'lower', 'released', ]), - 'friendly_name': 'Remote Control Switch 1', + 'friendly_name': 'Remote Control Kitchen Light', }), 'context': , - 'entity_id': 'event.remote_control_switch_1', + 'entity_id': 'event.remote_control_kitchen_light', 'last_changed': , 'last_reported': , 'last_updated': , From 6738085391bb3b84c6705ffe275b284322166a7d Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 20 Jun 2025 10:54:11 -0500 Subject: [PATCH 59/69] Minor clean up missed in previous PR (#147229) --- homeassistant/components/assist_satellite/__init__.py | 2 +- homeassistant/components/assist_satellite/entity.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/assist_satellite/__init__.py b/homeassistant/components/assist_satellite/__init__.py index f1f38f343f9..6bfbdfb33a8 100644 --- a/homeassistant/components/assist_satellite/__init__.py +++ b/homeassistant/components/assist_satellite/__init__.py @@ -133,7 +133,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: service_func=handle_ask_question, schema=vol.All( { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN), vol.Optional("question"): str, vol.Optional("question_media_id"): str, vol.Optional("preannounce"): bool, diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index d32bad2c824..e7a10ef63f6 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -138,7 +138,6 @@ class AssistSatelliteEntity(entity.Entity): _is_announcing = False _extra_system_prompt: str | None = None _wake_word_intercept_future: asyncio.Future[str | None] | None = None - _stt_intercept_future: asyncio.Future[str | None] | None = None _attr_tts_options: dict[str, Any] | None = None _pipeline_task: asyncio.Task | None = None _ask_question_future: asyncio.Future[str | None] | None = None From 9346c584c381d826260a55f6a8891d7e5f35da86 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:42:47 +0200 Subject: [PATCH 60/69] Add reconfigure flow to ntfy integration (#143743) --- homeassistant/components/ntfy/config_flow.py | 115 +++++++++ .../components/ntfy/quality_scale.yaml | 2 +- homeassistant/components/ntfy/strings.json | 31 ++- tests/components/ntfy/test_config_flow.py | 233 ++++++++++++++++++ 4 files changed, 378 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ntfy/config_flow.py b/homeassistant/components/ntfy/config_flow.py index 04a6730aa73..ed8d56820c2 100644 --- a/homeassistant/components/ntfy/config_flow.py +++ b/homeassistant/components/ntfy/config_flow.py @@ -90,6 +90,24 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema( } ) +STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema( + { + vol.Exclusive(CONF_USERNAME, ATTR_CREDENTIALS): TextSelector( + TextSelectorConfig( + type=TextSelectorType.TEXT, + autocomplete="username", + ), + ), + vol.Optional(CONF_PASSWORD, default=""): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ), + ), + vol.Exclusive(CONF_TOKEN, ATTR_CREDENTIALS): str, + } +) + STEP_USER_TOPIC_SCHEMA = vol.Schema( { vol.Required(CONF_TOPIC): str, @@ -244,6 +262,103 @@ class NtfyConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={CONF_USERNAME: entry.data[CONF_USERNAME]}, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfigure flow for ntfy.""" + errors: dict[str, str] = {} + + entry = self._get_reconfigure_entry() + + if user_input is not None: + session = async_get_clientsession(self.hass) + if token := user_input.get(CONF_TOKEN): + ntfy = Ntfy( + entry.data[CONF_URL], + session, + token=user_input[CONF_TOKEN], + ) + else: + ntfy = Ntfy( + entry.data[CONF_URL], + session, + username=user_input.get(CONF_USERNAME, entry.data[CONF_USERNAME]), + password=user_input[CONF_PASSWORD], + ) + + try: + account = await ntfy.account() + if not token: + token = (await ntfy.generate_token("Home Assistant")).token + except NtfyUnauthorizedAuthenticationError: + errors["base"] = "invalid_auth" + except NtfyHTTPError as e: + _LOGGER.debug("Error %s: %s [%s]", e.code, e.error, e.link) + errors["base"] = "cannot_connect" + except NtfyException: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + if entry.data[CONF_USERNAME]: + if entry.data[CONF_USERNAME] != account.username: + return self.async_abort( + reason="account_mismatch", + description_placeholders={ + CONF_USERNAME: entry.data[CONF_USERNAME], + "wrong_username": account.username, + }, + ) + + return self.async_update_reload_and_abort( + entry, + data_updates={CONF_TOKEN: token}, + ) + self._async_abort_entries_match( + { + CONF_URL: entry.data[CONF_URL], + CONF_USERNAME: account.username, + } + ) + return self.async_update_reload_and_abort( + entry, + data_updates={ + CONF_USERNAME: account.username, + CONF_TOKEN: token, + }, + ) + if entry.data[CONF_USERNAME]: + return self.async_show_form( + step_id="reconfigure_user", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_REAUTH_DATA_SCHEMA, + suggested_values=user_input, + ), + errors=errors, + description_placeholders={ + CONF_NAME: entry.title, + CONF_USERNAME: entry.data[CONF_USERNAME], + }, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_RECONFIGURE_DATA_SCHEMA, + suggested_values=user_input, + ), + errors=errors, + description_placeholders={CONF_NAME: entry.title}, + ) + + async def async_step_reconfigure_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfigure flow for authenticated ntfy entry.""" + + return await self.async_step_reconfigure(user_input) + class TopicSubentryFlowHandler(ConfigSubentryFlow): """Handle subentry flow for adding and modifying a topic.""" diff --git a/homeassistant/components/ntfy/quality_scale.yaml b/homeassistant/components/ntfy/quality_scale.yaml index 0d075f0014b..43a96135baf 100644 --- a/homeassistant/components/ntfy/quality_scale.yaml +++ b/homeassistant/components/ntfy/quality_scale.yaml @@ -72,7 +72,7 @@ rules: comment: the notify entity uses the device name as entity name, no translation required exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: the integration has no repairs diff --git a/homeassistant/components/ntfy/strings.json b/homeassistant/components/ntfy/strings.json index 13704d960be..cef662d6f2f 100644 --- a/homeassistant/components/ntfy/strings.json +++ b/homeassistant/components/ntfy/strings.json @@ -39,7 +39,33 @@ }, "data_description": { "password": "Enter the password corresponding to the aforementioned username to automatically create an access token", - "token": "Enter a new access token. To create a new access token navigate to Account → Access tokens and click create access token" + "token": "Enter a new access token. To create a new access token navigate to Account → Access tokens and select 'Create access token'" + } + }, + "reconfigure": { + "title": "Configuration for {name}", + "description": "You can either log in with your **ntfy** username and password, and Home Assistant will automatically create an access token to authenticate with **ntfy**, or you can provide an access token directly", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "token": "[%key:common::config_flow::data::access_token%]" + }, + "data_description": { + "username": "[%key:component::ntfy::config::step::user::sections::auth::data_description::username%]", + "password": "[%key:component::ntfy::config::step::user::sections::auth::data_description::password%]", + "token": "Enter a new or existing access token. To create a new access token navigate to Account → Access tokens and select 'Create access token'" + } + }, + "reconfigure_user": { + "title": "[%key:component::ntfy::config::step::reconfigure::title%]", + "description": "Enter the password for **{username}** below. Home Assistant will automatically create a new access token to authenticate with **ntfy**. You can also directly provide a valid access token", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "token": "[%key:common::config_flow::data::access_token%]" + }, + "data_description": { + "password": "[%key:component::ntfy::config::step::reauth_confirm::data_description::password%]", + "token": "[%key:component::ntfy::config::step::reconfigure::data_description::token%]" } } }, @@ -51,7 +77,8 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "account_mismatch": "The provided access token corresponds to the account {wrong_username}. Please re-authenticate with the account **{username}**" + "account_mismatch": "The provided access token corresponds to the account {wrong_username}. Please re-authenticate with the account **{username}**", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "config_subentries": { diff --git a/tests/components/ntfy/test_config_flow.py b/tests/components/ntfy/test_config_flow.py index 2d3656536a9..48909552e08 100644 --- a/tests/components/ntfy/test_config_flow.py +++ b/tests/components/ntfy/test_config_flow.py @@ -498,3 +498,236 @@ async def test_flow_reauth_account_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "account_mismatch" + + +@pytest.mark.parametrize( + ("entry_data", "user_input", "step_id"), + [ + ( + {CONF_USERNAME: None, CONF_TOKEN: None}, + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + "reconfigure", + ), + ( + {CONF_USERNAME: "username", CONF_TOKEN: "oldtoken"}, + {CONF_TOKEN: "newtoken"}, + "reconfigure_user", + ), + ], +) +async def test_flow_reconfigure( + hass: HomeAssistant, + mock_aiontfy: AsyncMock, + entry_data: dict[str, str | None], + user_input: dict[str, str], + step_id: str, +) -> None: + """Test reconfigure flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="ntfy.sh", + data={ + CONF_URL: "https://ntfy.sh/", + **entry_data, + }, + ) + mock_aiontfy.generate_token.return_value = AccountTokenResponse( + token="newtoken", last_access=datetime.now() + ) + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == step_id + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_USERNAME] == "username" + assert config_entry.data[CONF_TOKEN] == "newtoken" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("entry_data", "step_id"), + [ + ({CONF_USERNAME: None, CONF_TOKEN: None}, "reconfigure"), + ({CONF_USERNAME: "username", CONF_TOKEN: "oldtoken"}, "reconfigure_user"), + ], +) +@pytest.mark.usefixtures("mock_aiontfy") +async def test_flow_reconfigure_token( + hass: HomeAssistant, + entry_data: dict[str, Any], + step_id: str, +) -> None: + """Test reconfigure flow with access token.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="ntfy.sh", + data={ + CONF_URL: "https://ntfy.sh/", + **entry_data, + }, + ) + + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == step_id + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "access_token"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_USERNAME] == "username" + assert config_entry.data[CONF_TOKEN] == "access_token" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + ( + NtfyHTTPError(418001, 418, "I'm a teapot", ""), + "cannot_connect", + ), + ( + NtfyUnauthorizedAuthenticationError( + 40101, + 401, + "unauthorized", + "https://ntfy.sh/docs/publish/#authentication", + ), + "invalid_auth", + ), + (NtfyException, "cannot_connect"), + (TypeError, "unknown"), + ], +) +async def test_flow_reconfigure_errors( + hass: HomeAssistant, + mock_aiontfy: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test reconfigure flow errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="ntfy.sh", + data={ + CONF_URL: "https://ntfy.sh/", + CONF_USERNAME: None, + CONF_TOKEN: None, + }, + ) + mock_aiontfy.generate_token.return_value = AccountTokenResponse( + token="newtoken", last_access=datetime.now() + ) + mock_aiontfy.account.side_effect = exception + + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_aiontfy.account.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_USERNAME] == "username" + assert config_entry.data[CONF_TOKEN] == "newtoken" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.usefixtures("mock_aiontfy") +async def test_flow_reconfigure_already_configured( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow already configured.""" + other_config_entry = MockConfigEntry( + domain=DOMAIN, + title="ntfy.sh", + data={ + CONF_URL: "https://ntfy.sh/", + CONF_USERNAME: "username", + }, + ) + other_config_entry.add_to_hass(hass) + + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert len(hass.config_entries.async_entries()) == 2 + + +@pytest.mark.usefixtures("mock_aiontfy") +async def test_flow_reconfigure_account_mismatch( + hass: HomeAssistant, +) -> None: + """Test reconfigure flow account mismatch.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="ntfy.sh", + data={ + CONF_URL: "https://ntfy.sh/", + CONF_USERNAME: "wrong_username", + CONF_TOKEN: "oldtoken", + }, + ) + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "newtoken"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "account_mismatch" From 95f292c43d8d5ee485f99c74d6fda717890b0c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Fri, 20 Jun 2025 19:27:29 +0200 Subject: [PATCH 61/69] Bump aiohomeconnect to 0.18.1 (#147236) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 5e296ba18ac..8ced21ecba5 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -22,6 +22,6 @@ "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], "quality_scale": "platinum", - "requirements": ["aiohomeconnect==0.18.0"], + "requirements": ["aiohomeconnect==0.18.1"], "zeroconf": ["_homeconnect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 90e902ed8c7..6d1d834a8e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.18.0 +aiohomeconnect==0.18.1 # homeassistant.components.homekit_controller aiohomekit==3.2.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 633d6904fc7..3ad1ef1ed87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.18.0 +aiohomeconnect==0.18.1 # homeassistant.components.homekit_controller aiohomekit==3.2.15 From 435c08685d1fecb6b196bf1f6c4cf7e1042c2412 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 20 Jun 2025 20:22:33 +0200 Subject: [PATCH 62/69] Bump deebot-client to 13.4.0 (#147221) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 8a7388da735..97739f698d9 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==13.3.0"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6d1d834a8e7..a1fa1694dd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ decora-wifi==1.4 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.3.0 +deebot-client==13.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ad1ef1ed87..492978d9000 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -665,7 +665,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.3.0 +deebot-client==13.4.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 65f897793d184231fa41031b956d73c1ad42775d Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 20 Jun 2025 14:18:03 -0500 Subject: [PATCH 63/69] Use string instead of boolean for voice event (#147244) Use string instead of bool --- homeassistant/components/esphome/assist_satellite.py | 6 +++--- tests/components/esphome/test_assist_satellite.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/assist_satellite.py b/homeassistant/components/esphome/assist_satellite.py index fdeadd7feb1..f6367165400 100644 --- a/homeassistant/components/esphome/assist_satellite.py +++ b/homeassistant/components/esphome/assist_satellite.py @@ -285,9 +285,9 @@ class EsphomeAssistSatellite( data_to_send = {"text": event.data["stt_output"]["text"]} elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: data_to_send = { - "tts_start_streaming": bool( - event.data and event.data.get("tts_start_streaming") - ), + "tts_start_streaming": "1" + if (event.data and event.data.get("tts_start_streaming")) + else "0", } elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: assert event.data is not None diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index 71977f0285c..3acdc1f2029 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -243,12 +243,12 @@ async def test_pipeline_api_audio( event_callback( PipelineEvent( type=PipelineEventType.INTENT_PROGRESS, - data={"tts_start_streaming": True}, + data={"tts_start_streaming": "1"}, ) ) assert mock_client.send_voice_assistant_event.call_args_list[-1].args == ( VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS, - {"tts_start_streaming": True}, + {"tts_start_streaming": "1"}, ) event_callback( From ace18e540b797ce1088bddeb01ad69b27085a55e Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:59:59 -0400 Subject: [PATCH 64/69] Bump aiorussound to 4.6.1 (#147233) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 30b9205f439..a74a1887836 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_push", "loggers": ["aiorussound"], "quality_scale": "silver", - "requirements": ["aiorussound==4.6.0"], + "requirements": ["aiorussound==4.6.1"], "zeroconf": ["_rio._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a1fa1694dd6..4eca8e12383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -369,7 +369,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.6.0 +aiorussound==4.6.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 492978d9000..c63e4f2f551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -351,7 +351,7 @@ aioridwell==2024.01.0 aioruckus==0.42 # homeassistant.components.russound_rio -aiorussound==4.6.0 +aiorussound==4.6.1 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 From 9bcd74c44946f80d48e214338c634f62db6530ed Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Fri, 20 Jun 2025 15:39:22 -0500 Subject: [PATCH 65/69] Change async_supports_streaming_input to an instance method (#147245) --- homeassistant/components/tts/entity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tts/entity.py b/homeassistant/components/tts/entity.py index 2c3fd446d2f..dc6f22570fc 100644 --- a/homeassistant/components/tts/entity.py +++ b/homeassistant/components/tts/entity.py @@ -89,11 +89,11 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH """Return a mapping with the default options.""" return self._attr_default_options - @classmethod - def async_supports_streaming_input(cls) -> bool: + def async_supports_streaming_input(self) -> bool: """Return if the TTS engine supports streaming input.""" return ( - cls.async_stream_tts_audio is not TextToSpeechEntity.async_stream_tts_audio + self.__class__.async_stream_tts_audio + is not TextToSpeechEntity.async_stream_tts_audio ) @callback From 2e5de732a71191db26c31c4fabf97cd18fb4ee71 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Sat, 21 Jun 2025 01:32:14 +0200 Subject: [PATCH 66/69] Bump pyHomee to version 1.2.10 (#147248) bump pyHomee to version 1.2.10 --- homeassistant/components/homee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homee/manifest.json b/homeassistant/components/homee/manifest.json index 16ee6085439..16169676835 100644 --- a/homeassistant/components/homee/manifest.json +++ b/homeassistant/components/homee/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["homee"], "quality_scale": "bronze", - "requirements": ["pyHomee==1.2.9"] + "requirements": ["pyHomee==1.2.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4eca8e12383..cd9ff2a1a13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1799,7 +1799,7 @@ pyEmby==1.10 pyHik==0.3.2 # homeassistant.components.homee -pyHomee==1.2.9 +pyHomee==1.2.10 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c63e4f2f551..e01054aafd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1510,7 +1510,7 @@ pyDuotecno==2024.10.1 pyElectra==1.2.4 # homeassistant.components.homee -pyHomee==1.2.9 +pyHomee==1.2.10 # homeassistant.components.rfxtrx pyRFXtrx==0.31.1 From 7442f7af28d28a2f6e5417f4cb45e08476be63ac Mon Sep 17 00:00:00 2001 From: hanwg Date: Sat, 21 Jun 2025 09:21:10 +0800 Subject: [PATCH 67/69] Fix Telegram bot parsing of inline keyboard (#146376) * bug fix for inline keyboard * update inline keyboard test * Update tests/components/telegram_bot/test_telegram_bot.py Co-authored-by: Martin Hjelmare * revert last_message_id and updated tests * removed TypeError test --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/telegram_bot/bot.py | 4 +- .../telegram_bot/test_telegram_bot.py | 130 +++++++++++++++++- 2 files changed, 127 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/telegram_bot/bot.py b/homeassistant/components/telegram_bot/bot.py index 534923b3568..f313972635f 100644 --- a/homeassistant/components/telegram_bot/bot.py +++ b/homeassistant/components/telegram_bot/bot.py @@ -321,8 +321,8 @@ class TelegramNotificationService: for key in row_keyboard.split(","): if ":/" in key: # check if command or URL - if key.startswith("https://"): - label = key.split(",")[0] + if "https://" in key: + label = key.split(":")[0] url = key[len(label) + 1 :] buttons.append(InlineKeyboardButton(label, url=url)) else: diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index 24b6deb27b5..fd313867561 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -1,12 +1,14 @@ """Tests for the telegram_bot component.""" import base64 +from datetime import datetime import io from typing import Any from unittest.mock import AsyncMock, MagicMock, mock_open, patch import pytest -from telegram import Update +from telegram import Chat, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update +from telegram.constants import ChatType, ParseMode from telegram.error import ( InvalidToken, NetworkError, @@ -16,28 +18,37 @@ from telegram.error import ( ) from homeassistant.components.telegram_bot import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, + async_setup_entry, +) +from homeassistant.components.telegram_bot.const import ( ATTR_AUTHENTICATION, ATTR_CALLBACK_QUERY_ID, ATTR_CAPTION, ATTR_CHAT_ID, + ATTR_DISABLE_NOTIF, + ATTR_DISABLE_WEB_PREV, ATTR_FILE, + ATTR_KEYBOARD, ATTR_KEYBOARD_INLINE, - ATTR_LATITUDE, - ATTR_LONGITUDE, ATTR_MESSAGE, + ATTR_MESSAGE_TAG, ATTR_MESSAGE_THREAD_ID, ATTR_MESSAGEID, ATTR_OPTIONS, + ATTR_PARSER, ATTR_PASSWORD, ATTR_QUESTION, + ATTR_REPLY_TO_MSGID, ATTR_SHOW_ALERT, ATTR_STICKER_ID, ATTR_TARGET, + ATTR_TIMEOUT, ATTR_URL, ATTR_USERNAME, ATTR_VERIFY_SSL, CONF_CONFIG_ENTRY_ID, - CONF_PLATFORM, DOMAIN, PLATFORM_BROADCAST, SERVICE_ANSWER_CALLBACK_QUERY, @@ -55,12 +66,12 @@ from homeassistant.components.telegram_bot import ( SERVICE_SEND_STICKER, SERVICE_SEND_VIDEO, SERVICE_SEND_VOICE, - async_setup_entry, ) from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_API_KEY, + CONF_PLATFORM, HTTP_BASIC_AUTHENTICATION, HTTP_BEARER_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, @@ -96,6 +107,26 @@ async def test_polling_platform_init(hass: HomeAssistant, polling_platform) -> N SERVICE_SEND_MESSAGE, {ATTR_MESSAGE: "test_message", ATTR_MESSAGE_THREAD_ID: "123"}, ), + ( + SERVICE_SEND_MESSAGE, + { + ATTR_KEYBOARD: ["/command1, /command2", "/command3"], + ATTR_MESSAGE: "test_message", + ATTR_PARSER: ParseMode.HTML, + ATTR_TIMEOUT: 15, + ATTR_DISABLE_NOTIF: True, + ATTR_DISABLE_WEB_PREV: True, + ATTR_MESSAGE_TAG: "mock_tag", + ATTR_REPLY_TO_MSGID: 12345, + }, + ), + ( + SERVICE_SEND_MESSAGE, + { + ATTR_KEYBOARD: [], + ATTR_MESSAGE: "test_message", + }, + ), ( SERVICE_SEND_STICKER, { @@ -145,6 +176,95 @@ async def test_send_message( assert (response["chats"][0]["message_id"]) == 12345 +@pytest.mark.parametrize( + ("input", "expected"), + [ + ( + { + ATTR_MESSAGE: "test_message", + ATTR_KEYBOARD_INLINE: "command1:/cmd1,/cmd2,mock_link:https://mock_link", + }, + InlineKeyboardMarkup( + # 1 row with 3 buttons + [ + [ + InlineKeyboardButton(callback_data="/cmd1", text="command1"), + InlineKeyboardButton(callback_data="/cmd2", text="CMD2"), + InlineKeyboardButton(url="https://mock_link", text="mock_link"), + ] + ] + ), + ), + ( + { + ATTR_MESSAGE: "test_message", + ATTR_KEYBOARD_INLINE: [ + [["command1", "/cmd1"]], + [["mock_link", "https://mock_link"]], + ], + }, + InlineKeyboardMarkup( + # 2 rows each with 1 button + [ + [InlineKeyboardButton(callback_data="/cmd1", text="command1")], + [InlineKeyboardButton(url="https://mock_link", text="mock_link")], + ] + ), + ), + ], +) +async def test_send_message_with_inline_keyboard( + hass: HomeAssistant, + webhook_platform, + input: dict[str, Any], + expected: InlineKeyboardMarkup, +) -> None: + """Test the send_message service. + + Tests any service that does not require files to be sent. + """ + context = Context() + events = async_capture_events(hass, "telegram_sent") + + with patch( + "homeassistant.components.telegram_bot.bot.Bot.send_message", + AsyncMock( + return_value=Message( + message_id=12345, + date=datetime.now(), + chat=Chat(id=123456, type=ChatType.PRIVATE), + ) + ), + ) as mock_send_message: + response = await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MESSAGE, + input, + blocking=True, + context=context, + return_response=True, + ) + await hass.async_block_till_done() + + mock_send_message.assert_called_once_with( + 12345678, + "test_message", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=None, + disable_notification=False, + reply_to_message_id=None, + reply_markup=expected, + read_timeout=None, + message_thread_id=None, + ) + + assert len(events) == 1 + assert events[0].context == context + + assert len(response["chats"]) == 1 + assert (response["chats"][0]["message_id"]) == 12345 + + @patch( "builtins.open", mock_open( From 79a9f34150e3ae92e4ef651b127f9908093ae1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 21 Jun 2025 10:53:17 +0100 Subject: [PATCH 68/69] Handle the new JSON payload from traccar clients (#147254) --- homeassistant/components/traccar/__init__.py | 42 ++++++++++++++--- homeassistant/components/traccar/const.py | 1 - tests/components/traccar/test_init.py | 47 +++++++++++++++++++- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 5b9bc2551b7..e8c151179ce 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,9 +1,12 @@ """Support for Traccar Client.""" from http import HTTPStatus +from json import JSONDecodeError +import logging from aiohttp import web import voluptuous as vol +from voluptuous.humanize import humanize_error from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry @@ -20,7 +23,6 @@ from .const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_SPEED, - ATTR_TIMESTAMP, DOMAIN, ) @@ -29,6 +31,7 @@ PLATFORMS = [Platform.DEVICE_TRACKER] TRACKER_UPDATE = f"{DOMAIN}_tracker_update" +LOGGER = logging.getLogger(__name__) DEFAULT_ACCURACY = 200 DEFAULT_BATTERY = -1 @@ -49,21 +52,50 @@ WEBHOOK_SCHEMA = vol.Schema( vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), vol.Optional(ATTR_BEARING): vol.Coerce(float), vol.Optional(ATTR_SPEED): vol.Coerce(float), - vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int), }, extra=vol.REMOVE_EXTRA, ) +def _parse_json_body(json_body: dict) -> dict: + """Parse JSON body from request.""" + location = json_body.get("location", {}) + coords = location.get("coords", {}) + battery_level = location.get("battery", {}).get("level") + return { + "id": json_body.get("device_id"), + "lat": coords.get("latitude"), + "lon": coords.get("longitude"), + "accuracy": coords.get("accuracy"), + "altitude": coords.get("altitude"), + "batt": battery_level * 100 if battery_level is not None else DEFAULT_BATTERY, + "bearing": coords.get("heading"), + "speed": coords.get("speed"), + } + + async def handle_webhook( - hass: HomeAssistant, webhook_id: str, request: web.Request + hass: HomeAssistant, + webhook_id: str, + request: web.Request, ) -> web.Response: """Handle incoming webhook with Traccar Client request.""" + if not (requestdata := dict(request.query)): + try: + requestdata = _parse_json_body(await request.json()) + except JSONDecodeError as error: + LOGGER.error("Error parsing JSON body: %s", error) + return web.Response( + text="Invalid JSON", + status=HTTPStatus.UNPROCESSABLE_ENTITY, + ) try: - data = WEBHOOK_SCHEMA(dict(request.query)) + data = WEBHOOK_SCHEMA(requestdata) except vol.MultipleInvalid as error: + LOGGER.warning(humanize_error(requestdata, error)) return web.Response( - text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + text=error.error_message, + status=HTTPStatus.UNPROCESSABLE_ENTITY, ) attrs = { diff --git a/homeassistant/components/traccar/const.py b/homeassistant/components/traccar/const.py index df4bfa8ec99..f6928cc9ee9 100644 --- a/homeassistant/components/traccar/const.py +++ b/homeassistant/components/traccar/const.py @@ -17,7 +17,6 @@ ATTR_LONGITUDE = "lon" ATTR_MOTION = "motion" ATTR_SPEED = "speed" ATTR_STATUS = "status" -ATTR_TIMESTAMP = "timestamp" ATTR_TRACKER = "tracker" ATTR_TRACCAR_ID = "traccar_id" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index fb90262a084..eb864cadd87 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -146,8 +146,12 @@ async def test_enter_and_exit( assert len(entity_registry.entities) == 1 -async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None: - """Test when additional attributes are present.""" +async def test_enter_with_attrs_as_query( + hass: HomeAssistant, + client, + webhook_id, +) -> None: + """Test when additional attributes are present URL query.""" url = f"/api/webhook/{webhook_id}" data = { "timestamp": 123456789, @@ -197,6 +201,45 @@ async def test_enter_with_attrs(hass: HomeAssistant, client, webhook_id) -> None assert state.attributes["altitude"] == 123 +async def test_enter_with_attrs_as_payload( + hass: HomeAssistant, client, webhook_id +) -> None: + """Test when additional attributes are present in JSON payload.""" + url = f"/api/webhook/{webhook_id}" + data = { + "location": { + "coords": { + "heading": "105.32", + "latitude": "1.0", + "longitude": "1.1", + "accuracy": 10.5, + "altitude": 102.0, + "speed": 100.0, + }, + "extras": {}, + "manual": True, + "is_moving": False, + "_": "&id=123&lat=1.0&lon=1.1×tamp=2013-09-17T07:32:51Z&", + "odometer": 0, + "activity": {"type": "still"}, + "timestamp": "2013-09-17T07:32:51Z", + "battery": {"level": 0.1, "is_charging": False}, + }, + "device_id": "123", + } + + req = await client.post(url, json=data) + await hass.async_block_till_done() + assert req.status == HTTPStatus.OK + state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device_id']}") + assert state.state == STATE_NOT_HOME + assert state.attributes["gps_accuracy"] == 10.5 + assert state.attributes["battery_level"] == 10.0 + assert state.attributes["speed"] == 100.0 + assert state.attributes["bearing"] == 105.32 + assert state.attributes["altitude"] == 102.0 + + async def test_two_devices(hass: HomeAssistant, client, webhook_id) -> None: """Test updating two different devices.""" url = f"/api/webhook/{webhook_id}" From c453eed32df9c13bd3689741cf2be1c71b4d944f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 21 Jun 2025 16:44:22 +0300 Subject: [PATCH 69/69] Bump aioamazondevices to 3.1.14 (#147257) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index aeecb5bc96c..a2bb423860b 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.1.12"] + "requirements": ["aioamazondevices==3.1.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd9ff2a1a13..c1663ab70e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.12 +aioamazondevices==3.1.14 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e01054aafd2..066a64a2713 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.alexa_devices -aioamazondevices==3.1.12 +aioamazondevices==3.1.14 # homeassistant.components.ambient_network # homeassistant.components.ambient_station