diff --git a/pyproject.toml b/pyproject.toml index c55de25bb..095a228ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,18 +99,6 @@ non_interactive = true # `python -m tools.regenerate_mypy_ignores`. They are sorted easiest-to-fix # first; clean up a module's errors and re-run the tool to shrink the list. # Autogenerated mypy overrides: start -[[tool.mypy.overrides]] -module = "zha.application.platforms.button" -disable_error_code = [ - "assignment", # 1 -] - -[[tool.mypy.overrides]] -module = "zha.application.platforms.update" -disable_error_code = [ - "arg-type", # 1 -] - [[tool.mypy.overrides]] module = "zha.application.platforms.virtual" disable_error_code = [ @@ -129,12 +117,6 @@ disable_error_code = [ "no-any-return", # 2 ] -[[tool.mypy.overrides]] -module = "zha.application.platforms.siren" -disable_error_code = [ - "assignment", # 2 -] - [[tool.mypy.overrides]] module = "zha.application.discovery" disable_error_code = [ @@ -142,17 +124,18 @@ disable_error_code = [ ] [[tool.mypy.overrides]] -module = "zha.application.platforms.binary_sensor" +module = "zha.application.platforms" disable_error_code = [ + "arg-type", # 1 + "assignment", # 1 "no-any-return", # 1 - "no-untyped-call", # 1 - "no-untyped-def", # 1 ] [[tool.mypy.overrides]] -module = "zha.application.platforms.lock" +module = "zha.application.platforms.binary_sensor" disable_error_code = [ - "arg-type", # 2 + "no-any-return", # 1 + "no-untyped-call", # 1 "no-untyped-def", # 1 ] @@ -170,14 +153,6 @@ disable_error_code = [ "no-untyped-call", # 1 ] -[[tool.mypy.overrides]] -module = "zha.application.platforms" -disable_error_code = [ - "arg-type", # 1 - "assignment", # 1 - "no-any-return", # 2 -] - [[tool.mypy.overrides]] module = "zha.application.platforms.device_tracker" disable_error_code = [ @@ -250,7 +225,7 @@ disable_error_code = [ [[tool.mypy.overrides]] module = "zha.application.platforms.light" disable_error_code = [ - "arg-type", # 5 + "arg-type", # 3 "no-any-return", # 4 "no-untyped-call", # 11 "no-untyped-def", # 8 @@ -268,8 +243,8 @@ disable_error_code = [ [[tool.mypy.overrides]] module = "zha.application.platforms.sensor" disable_error_code = [ - "arg-type", # 10 - "assignment", # 3 + "arg-type", # 12 + "assignment", # 2 "attr-defined", # 5 "no-any-return", # 9 "no-untyped-call", # 4 diff --git a/tests/common.py b/tests/common.py index cf8f499cd..5cbac97e1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -254,7 +254,7 @@ def get_group_entity( if not isinstance(entity, entity_type): continue - if qualifier is not None and qualifier not in entity.info_object.unique_id: + if qualifier is not None and qualifier not in entity.state.unique_id: continue return entity @@ -285,7 +285,7 @@ def get_entity( if exact_entity_type is not None and type(entity) is not exact_entity_type: continue - if qualifier is not None and qualifier not in entity.info_object.unique_id: + if qualifier is not None and qualifier not in entity.state.unique_id: continue if not qualifier_func(entity): @@ -311,51 +311,51 @@ async def group_entity_availability_test( ): """Test group entity availability handling.""" - assert entity.state["available"] is True + assert entity.state.available is True device_1.on_network = False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is True + assert entity.state.available is True device_2.on_network = False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is False + assert entity.state.available is False device_1.on_network = True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is True + assert entity.state.available is True device_2.on_network = True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is True + assert entity.state.available is True device_1.available = False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is True + assert entity.state.available is True device_2.available = False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is False + assert entity.state.available is False device_1.available = True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is True + assert entity.state.available is True device_2.available = True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["available"] is True + assert entity.state.available is True def zigpy_device_from_device_data( # noqa: C901 diff --git a/tests/test_alarm_control_panel.py b/tests/test_alarm_control_panel.py index 5192d4460..ae6a0fb6a 100644 --- a/tests/test_alarm_control_panel.py +++ b/tests/test_alarm_control_panel.py @@ -45,7 +45,7 @@ async def test_alarm_control_panel( assert isinstance(alarm_entity, AlarmControlPanel) # test that the state is STATE_ALARM_DISARMED - assert alarm_entity.state["state"] == AlarmState.DISARMED + assert alarm_entity.state.alarm_state == AlarmState.DISARMED # arm_away cluster.client_command.reset_mock() @@ -60,7 +60,7 @@ async def test_alarm_control_panel( security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, ) - assert alarm_entity.state["state"] == AlarmState.ARMED_AWAY + assert alarm_entity.state.alarm_state == AlarmState.ARMED_AWAY # type: ignore[comparison-overlap] # disarm await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -69,7 +69,7 @@ async def test_alarm_control_panel( cluster.client_command.reset_mock() await alarm_entity.async_alarm_arm_away("4321") await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_AWAY + assert alarm_entity.state.alarm_state == AlarmState.ARMED_AWAY cluster.client_command.reset_mock() # now simulate a faulty code entry sequence @@ -78,7 +78,7 @@ async def test_alarm_control_panel( await alarm_entity.async_alarm_disarm("0000") await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.TRIGGERED + assert alarm_entity.state.alarm_state == AlarmState.TRIGGERED assert cluster.client_command.call_count == 6 assert cluster.client_command.await_count == 6 assert cluster.client_command.call_args == call( @@ -95,7 +95,7 @@ async def test_alarm_control_panel( # arm_home await alarm_entity.async_alarm_arm_home("4321") await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_HOME + assert alarm_entity.state.alarm_state == AlarmState.ARMED_HOME assert cluster.client_command.call_count == 2 assert cluster.client_command.await_count == 2 assert cluster.client_command.call_args == call( @@ -112,7 +112,7 @@ async def test_alarm_control_panel( # arm_night await alarm_entity.async_alarm_arm_night("4321") await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_NIGHT + assert alarm_entity.state.alarm_state == AlarmState.ARMED_NIGHT assert cluster.client_command.call_count == 2 assert cluster.client_command.await_count == 2 assert cluster.client_command.call_args == call( @@ -131,7 +131,7 @@ async def test_alarm_control_panel( "cluster_command", 1, 0, [security.IasAce.ArmMode.Arm_All_Zones, "", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_AWAY + assert alarm_entity.state.alarm_state == AlarmState.ARMED_AWAY # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -141,7 +141,7 @@ async def test_alarm_control_panel( "cluster_command", 1, 0, [security.IasAce.ArmMode.Arm_Day_Home_Only, "", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_HOME + assert alarm_entity.state.alarm_state == AlarmState.ARMED_HOME # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -151,41 +151,41 @@ async def test_alarm_control_panel( "cluster_command", 1, 0, [security.IasAce.ArmMode.Arm_Night_Sleep_Only, "", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_NIGHT + assert alarm_entity.state.alarm_state == AlarmState.ARMED_NIGHT # disarm from panel with bad code cluster.listener_event( "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.ARMED_NIGHT + assert alarm_entity.state.alarm_state == AlarmState.ARMED_NIGHT # disarm from panel with bad code for 2nd time still armed cluster.listener_event( "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.TRIGGERED + assert alarm_entity.state.alarm_state == AlarmState.TRIGGERED # disarm from panel with good code cluster.listener_event( "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "4321", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.DISARMED + assert alarm_entity.state.alarm_state == AlarmState.DISARMED # disarm when already disarmed cluster.listener_event( "cluster_command", 1, 0, [security.IasAce.ArmMode.Disarm, "4321", 0] ) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.DISARMED + assert alarm_entity.state.alarm_state == AlarmState.DISARMED assert "IAS ACE already disarmed" in caplog.text # panic from panel cluster.listener_event("cluster_command", 1, 4, []) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.TRIGGERED + assert alarm_entity.state.alarm_state == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -193,7 +193,7 @@ async def test_alarm_control_panel( # fire from panel cluster.listener_event("cluster_command", 1, 3, []) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.TRIGGERED + assert alarm_entity.state.alarm_state == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -201,24 +201,24 @@ async def test_alarm_control_panel( # emergency from panel cluster.listener_event("cluster_command", 1, 2, []) await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.TRIGGERED + assert alarm_entity.state.alarm_state == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) - assert alarm_entity.state["state"] == AlarmState.DISARMED + assert alarm_entity.state.alarm_state == AlarmState.DISARMED await alarm_entity.async_alarm_trigger() await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.TRIGGERED + assert alarm_entity.state.alarm_state == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) - assert alarm_entity.state["state"] == AlarmState.DISARMED + assert alarm_entity.state.alarm_state == AlarmState.DISARMED alarm_entity.code_required_arm_actions = True await alarm_entity.async_alarm_arm_away() await zha_gateway.async_block_till_done() - assert alarm_entity.state["state"] == AlarmState.DISARMED + assert alarm_entity.state.alarm_state == AlarmState.DISARMED assert "Invalid code supplied to IAS ACE" in caplog.text @@ -231,7 +231,7 @@ async def reset_alarm_panel( cluster.client_command.reset_mock() await entity.async_alarm_disarm("4321") await zha_gateway.async_block_till_done() - assert entity.state["state"] == AlarmState.DISARMED + assert entity.state.alarm_state == AlarmState.DISARMED assert cluster.client_command.call_count == 2 assert cluster.client_command.await_count == 2 assert cluster.client_command.call_args == call( diff --git a/tests/test_climate.py b/tests/test_climate.py index 3109c298c..33aaa2114 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -271,10 +271,10 @@ async def test_climate_local_temperature( entity: ThermostatEntity = get_entity( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["current_temperature"] is None + assert entity.state.current_temperature is None await send_attributes_report(zha_gateway, thrm_cluster, {0: 2100}) - assert entity.state["current_temperature"] == 21.0 + assert entity.state.current_temperature == 21.0 async def test_climate_outdoor_temperature( @@ -286,14 +286,14 @@ async def test_climate_outdoor_temperature( entity: ThermostatEntity = get_entity( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["outdoor_temperature"] is None + assert entity.state.outdoor_temperature is None await send_attributes_report( zha_gateway, thrm_cluster, {Thermostat.AttributeDefs.outdoor_temperature.id: 2150}, ) - assert entity.state["outdoor_temperature"] == 21.5 + assert entity.state.outdoor_temperature == 21.5 async def test_climate_hvac_action_running_state( @@ -315,55 +315,55 @@ async def test_climate_hvac_action_running_state( entity.on_event(STATE_CHANGED, subscriber1) sensor_entity.on_event(STATE_CHANGED, subscriber2) - assert entity.state["hvac_action"] == "off" - assert sensor_entity.state["state"] == "off" + assert entity.state.hvac_action == "off" + assert sensor_entity.state.native_value == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["hvac_action"] == "off" - assert sensor_entity.state["state"] == "off" + assert entity.state.hvac_action == "off" + assert sensor_entity.state.native_value == "off" assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 0 await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto} ) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["hvac_action"] == "idle" - assert sensor_entity.state["state"] == "idle" + assert entity.state.hvac_action == "idle" + assert sensor_entity.state.native_value == "idle" assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 1 await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool} ) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["hvac_action"] == "cooling" - assert sensor_entity.state["state"] == "cooling" + assert entity.state.hvac_action == "cooling" + assert sensor_entity.state.native_value == "cooling" assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 2 await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat} ) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["hvac_action"] == "heating" - assert sensor_entity.state["state"] == "heating" + assert entity.state.hvac_action == "heating" + assert sensor_entity.state.native_value == "heating" assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 3 await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["hvac_action"] == "idle" - assert sensor_entity.state["state"] == "idle" + assert entity.state.hvac_action == "idle" + assert sensor_entity.state.native_value == "idle" assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 4 await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["hvac_action"] == "fan" - assert sensor_entity.state["state"] == "fan" + assert entity.state.hvac_action == "fan" + assert sensor_entity.state.native_value == "fan" assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 5 @@ -451,62 +451,62 @@ async def test_climate_hvac_action_running_state_zen( ) assert isinstance(sensor_entity, ThermostatHVACAction) - assert entity.state["hvac_action"] is None - assert sensor_entity.state["state"] is None + assert entity.state.hvac_action is None + assert sensor_entity.state.native_value is None await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} ) - assert entity.state["hvac_action"] == "cooling" - assert sensor_entity.state["state"] == "cooling" + assert entity.state.hvac_action == "cooling" + assert sensor_entity.state.native_value == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) - assert entity.state["hvac_action"] == "fan" - assert sensor_entity.state["state"] == "fan" + assert entity.state.hvac_action == "fan" + assert sensor_entity.state.native_value == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} ) - assert entity.state["hvac_action"] == "heating" - assert sensor_entity.state["state"] == "heating" + assert entity.state.hvac_action == "heating" + assert sensor_entity.state.native_value == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} ) - assert entity.state["hvac_action"] == "fan" - assert sensor_entity.state["state"] == "fan" + assert entity.state.hvac_action == "fan" + assert sensor_entity.state.native_value == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} ) - assert entity.state["hvac_action"] == "cooling" - assert sensor_entity.state["state"] == "cooling" + assert entity.state.hvac_action == "cooling" + assert sensor_entity.state.native_value == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} ) - assert entity.state["hvac_action"] == "fan" - assert sensor_entity.state["state"] == "fan" + assert entity.state.hvac_action == "fan" + assert sensor_entity.state.native_value == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} ) - assert entity.state["hvac_action"] == "heating" - assert sensor_entity.state["state"] == "heating" + assert entity.state.hvac_action == "heating" + assert sensor_entity.state.native_value == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} ) - assert entity.state["hvac_action"] == "off" - assert sensor_entity.state["state"] == "off" + assert entity.state.hvac_action == "off" + assert sensor_entity.state.native_value == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) - assert entity.state["hvac_action"] == "idle" - assert sensor_entity.state["state"] == "idle" + assert entity.state.hvac_action == "idle" + assert sensor_entity.state.native_value == "idle" async def test_climate_hvac_action_running_state_zehnder( @@ -523,52 +523,52 @@ async def test_climate_hvac_action_running_state_zehnder( device_climate_zehnder, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_action"] is None + assert entity.state.hvac_action is None await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} ) - assert entity.state["hvac_action"] == "cooling" + assert entity.state.hvac_action == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) - assert entity.state["hvac_action"] == "fan" + assert entity.state.hvac_action == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} ) - assert entity.state["hvac_action"] == "heating" + assert entity.state.hvac_action == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} ) - assert entity.state["hvac_action"] == "fan" + assert entity.state.hvac_action == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} ) - assert entity.state["hvac_action"] == "cooling" + assert entity.state.hvac_action == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} ) - assert entity.state["hvac_action"] == "fan" + assert entity.state.hvac_action == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} ) - assert entity.state["hvac_action"] == "heating" + assert entity.state.hvac_action == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} ) - assert entity.state["hvac_action"] == "off" + assert entity.state.hvac_action == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) - assert entity.state["hvac_action"] == "idle" + assert entity.state.hvac_action == "idle" @pytest.mark.parametrize( @@ -594,27 +594,27 @@ async def test_set_hvac_mode_zehnder( device_climate_zehnder, platform=Platform.CLIMATE, entity_type=ZehnderThermostat ) - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" await entity.async_set_hvac_mode(hvac_mode) await zha_gateway.async_block_till_done() if sys_mode is not None: - assert entity.state["hvac_mode"] == hvac_mode + assert entity.state.hvac_mode == hvac_mode assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == { "system_mode": sys_mode } else: assert thrm_cluster.write_attributes.call_count == 0 - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" # turn off thrm_cluster.write_attributes.reset_mock() await entity.async_set_hvac_mode("off") await zha_gateway.async_block_till_done() - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == { "system_mode": Thermostat.SystemMode.Off @@ -631,28 +631,28 @@ async def test_climate_hvac_action_pi_demand( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_action"] is None + assert entity.state.hvac_action is None await send_attributes_report(zha_gateway, thrm_cluster, {0x0007: 10}) - assert entity.state["hvac_action"] == "cooling" + assert entity.state.hvac_action == "cooling" await send_attributes_report(zha_gateway, thrm_cluster, {0x0008: 20}) - assert entity.state["hvac_action"] == "heating" + assert entity.state.hvac_action == "heating" await send_attributes_report(zha_gateway, thrm_cluster, {0x0007: 0}) await send_attributes_report(zha_gateway, thrm_cluster, {0x0008: 0}) - assert entity.state["hvac_action"] == "off" + assert entity.state.hvac_action == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) - assert entity.state["hvac_action"] == "idle" + assert entity.state.hvac_action == "idle" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Cool} ) - assert entity.state["hvac_action"] == "idle" + assert entity.state.hvac_action == "idle" @pytest.mark.parametrize( @@ -678,18 +678,18 @@ async def test_hvac_mode( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" await send_attributes_report(zha_gateway, thrm_cluster, {0x001C: sys_mode}) - assert entity.state["hvac_mode"] == hvac_mode + assert entity.state.hvac_mode == hvac_mode await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Off} ) - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" await send_attributes_report(zha_gateway, thrm_cluster, {0x001C: 0xFF}) - assert entity.state["hvac_mode"] is None + assert entity.state.hvac_mode is None @pytest.mark.parametrize( @@ -757,7 +757,7 @@ async def test_target_temperature( await entity.async_set_preset_mode(preset) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature"] == target_temp + assert entity.state.target_temperature == target_temp @pytest.mark.parametrize( @@ -794,7 +794,7 @@ async def test_target_temperature_high( await entity.async_set_preset_mode(preset) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_high"] == target_temp + assert entity.state.target_temperature_high == target_temp @pytest.mark.parametrize( @@ -831,7 +831,7 @@ async def test_target_temperature_low( await entity.async_set_preset_mode(preset) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] == target_temp + assert entity.state.target_temperature_low == target_temp @pytest.mark.parametrize( @@ -858,27 +858,27 @@ async def test_set_hvac_mode( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" await entity.async_set_hvac_mode(hvac_mode) await zha_gateway.async_block_till_done() if sys_mode is not None: - assert entity.state["hvac_mode"] == hvac_mode + assert entity.state.hvac_mode == hvac_mode assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == { "system_mode": sys_mode } else: assert thrm_cluster.write_attributes.call_count == 0 - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" # turn off thrm_cluster.write_attributes.reset_mock() await entity.async_set_hvac_mode("off") await zha_gateway.async_block_till_done() - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == { "system_mode": Thermostat.SystemMode.Off @@ -895,7 +895,7 @@ async def test_preset_setting( dev_climate_sinope, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" # unsuccessful occupancy change thrm_cluster.write_attributes.return_value = [ @@ -913,7 +913,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 0} @@ -925,7 +925,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() - assert entity.state["preset_mode"] == "away" + assert entity.state.preset_mode == "away" assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 0} @@ -947,7 +947,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("none") await zha_gateway.async_block_till_done() - assert entity.state["preset_mode"] == "away" + assert entity.state.preset_mode == "away" assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 1} @@ -960,7 +960,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("none") await zha_gateway.async_block_till_done() - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" assert thrm_cluster.write_attributes.call_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 1} @@ -975,11 +975,11 @@ async def test_preset_setting_invalid( dev_climate_sinope, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" await entity.async_set_preset_mode("invalid_preset") await zha_gateway.async_block_till_done() - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" assert thrm_cluster.write_attributes.call_count == 0 @@ -994,11 +994,11 @@ async def test_set_temperature_hvac_mode( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "off" + assert entity.state.hvac_mode == "off" await entity.async_set_temperature(hvac_mode="heat_cool", temperature=20) await zha_gateway.async_block_till_done() - assert entity.state["hvac_mode"] == "heat_cool" + assert entity.state.hvac_mode == "heat_cool" assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args[0][0] == { "system_mode": Thermostat.SystemMode.Auto @@ -1028,20 +1028,20 @@ async def test_set_temperature_heat_cool( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "heat_cool" + assert entity.state.hvac_mode == "heat_cool" await entity.async_set_temperature(temperature=20) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] == 20.0 - assert entity.state["target_temperature_high"] == 25.0 + assert entity.state.target_temperature_low == 20.0 + assert entity.state.target_temperature_high == 25.0 assert thrm_cluster.write_attributes.await_count == 0 await entity.async_set_temperature(target_temp_high=26, target_temp_low=19) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] == 19.0 - assert entity.state["target_temperature_high"] == 26.0 + assert entity.state.target_temperature_low == 19.0 + assert entity.state.target_temperature_high == 26.0 assert thrm_cluster.write_attributes.await_count == 2 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { "occupied_heating_setpoint": 1900 @@ -1057,8 +1057,8 @@ async def test_set_temperature_heat_cool( await entity.async_set_temperature(target_temp_high=30, target_temp_low=15) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] == 15.0 - assert entity.state["target_temperature_high"] == 30.0 + assert entity.state.target_temperature_low == 15.0 + assert entity.state.target_temperature_high == 30.0 assert thrm_cluster.write_attributes.await_count == 2 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { "unoccupied_heating_setpoint": 1500 @@ -1091,22 +1091,22 @@ async def test_set_temperature_heat( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "heat" + assert entity.state.hvac_mode == "heat" await entity.async_set_temperature(target_temp_high=30, target_temp_low=15) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 20.0 + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature == 20.0 assert thrm_cluster.write_attributes.await_count == 0 await entity.async_set_temperature(temperature=21) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 21.0 + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature == 21.0 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { "occupied_heating_setpoint": 2100 @@ -1119,9 +1119,9 @@ async def test_set_temperature_heat( await entity.async_set_temperature(temperature=22) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 22.0 + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature == 22.0 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { "unoccupied_heating_setpoint": 2200 @@ -1151,22 +1151,22 @@ async def test_set_temperature_cool( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "cool" + assert entity.state.hvac_mode == "cool" await entity.async_set_temperature(target_temp_high=30, target_temp_low=15) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 25.0 + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature == 25.0 assert thrm_cluster.write_attributes.await_count == 0 await entity.async_set_temperature(temperature=21) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 21.0 + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature == 21.0 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { "occupied_cooling_setpoint": 2100 @@ -1179,9 +1179,9 @@ async def test_set_temperature_cool( await entity.async_set_temperature(temperature=22) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] == 22.0 + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature == 22.0 assert thrm_cluster.write_attributes.await_count == 1 assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { "unoccupied_cooling_setpoint": 2200 @@ -1215,14 +1215,14 @@ async def test_set_temperature_wrong_mode( device_climate, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["hvac_mode"] == "dry" + assert entity.state.hvac_mode == "dry" await entity.async_set_temperature(temperature=24) await zha_gateway.async_block_till_done() - assert entity.state["target_temperature_low"] is None - assert entity.state["target_temperature_high"] is None - assert entity.state["target_temperature"] is None + assert entity.state.target_temperature_low is None + assert entity.state.target_temperature_high is None + assert entity.state.target_temperature is None assert thrm_cluster.write_attributes.await_count == 0 @@ -1236,20 +1236,20 @@ async def test_occupancy_reset( dev_climate_sinope, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() thrm_cluster.write_attributes.reset_mock() - assert entity.state["preset_mode"] == "away" + assert entity.state.preset_mode == "away" await send_attributes_report( zha_gateway, thrm_cluster, {"occupied_heating_setpoint": zigpy.types.uint16_t(1950)}, ) - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" async def test_fan_mode( @@ -1263,26 +1263,26 @@ async def test_fan_mode( ) assert set(entity.fan_modes) == {FanState.AUTO, FanState.ON} - assert entity.state["fan_mode"] == FanState.AUTO + assert entity.state.fan_mode == FanState.AUTO await send_attributes_report( zha_gateway, thrm_cluster, {"running_state": Thermostat.RunningState.Fan_State_On}, ) - assert entity.state["fan_mode"] == FanState.ON + assert entity.state.fan_mode == FanState.ON await send_attributes_report( zha_gateway, thrm_cluster, {"running_state": Thermostat.RunningState.Idle} ) - assert entity.state["fan_mode"] == FanState.AUTO + assert entity.state.fan_mode == FanState.AUTO await send_attributes_report( zha_gateway, thrm_cluster, {"running_state": Thermostat.RunningState.Fan_2nd_Stage_On}, ) - assert entity.state["fan_mode"] == FanState.ON + assert entity.state.fan_mode == FanState.ON async def test_set_fan_mode_not_supported( @@ -1310,7 +1310,7 @@ async def test_set_fan_mode( device_climate_fan, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["fan_mode"] == FanState.AUTO + assert entity.state.fan_mode == FanState.AUTO await entity.async_set_fan_mode(FanState.ON) await zha_gateway.async_block_till_done() @@ -1339,7 +1339,7 @@ async def test_set_moes_preset(zha_gateway: Gateway): device_climate_moes, platform=Platform.CLIMATE, entity_type=ThermostatEntity ) - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() @@ -1434,31 +1434,31 @@ async def test_set_moes_operation_mode(zha_gateway: Gateway): await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 0}) - assert entity.state["preset_mode"] == "away" + assert entity.state.preset_mode == "away" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 1}) - assert entity.state["preset_mode"] == "Schedule" + assert entity.state.preset_mode == "Schedule" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 2}) - assert entity.state["preset_mode"] == "none" + assert entity.state.preset_mode == "none" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 3}) - assert entity.state["preset_mode"] == "comfort" + assert entity.state.preset_mode == "comfort" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 4}) - assert entity.state["preset_mode"] == "eco" + assert entity.state.preset_mode == "eco" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 5}) - assert entity.state["preset_mode"] == "boost" + assert entity.state.preset_mode == "boost" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 6}) - assert entity.state["preset_mode"] == "Complex" + assert entity.state.preset_mode == "Complex" # Device is running an energy-saving mode @@ -1498,7 +1498,7 @@ async def test_beca_operation_mode_update( zha_gateway, thrm_cluster, {"operation_preset": preset_attr} ) - assert entity.state[ATTR_PRESET_MODE] == preset_mode + assert entity.state.preset_mode == preset_mode await entity.async_set_preset_mode(preset_mode) await zha_gateway.async_block_till_done() @@ -1526,7 +1526,7 @@ async def test_set_zonnsmart_preset(zha_gateway: Gateway) -> None: entity_type=ThermostatEntity, ) - assert entity.state[ATTR_PRESET_MODE] == PRESET_NONE + assert entity.state.preset_mode == PRESET_NONE await entity.async_set_preset_mode(PRESET_SCHEDULE) await zha_gateway.async_block_till_done() @@ -1588,23 +1588,23 @@ async def test_set_zonnsmart_operation_mode(zha_gateway: Gateway) -> None: await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 0}) - assert entity.state[ATTR_PRESET_MODE] == PRESET_SCHEDULE + assert entity.state.preset_mode == PRESET_SCHEDULE await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 1}) - assert entity.state[ATTR_PRESET_MODE] == PRESET_NONE + assert entity.state.preset_mode == PRESET_NONE await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 2}) - assert entity.state[ATTR_PRESET_MODE] == "holiday" + assert entity.state.preset_mode == "holiday" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 3}) - assert entity.state[ATTR_PRESET_MODE] == "holiday" + assert entity.state.preset_mode == "holiday" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 4}) - assert entity.state[ATTR_PRESET_MODE] == "frost protect" + assert entity.state.preset_mode == "frost protect" async def test_thermostat_default_local_temperature_calibration_config( @@ -1625,9 +1625,9 @@ async def test_thermostat_default_local_temperature_calibration_config( ] assert local_temperature_calibration_entity assert isinstance(local_temperature_calibration_entity, NumberConfigurationEntity) - assert local_temperature_calibration_entity.info_object.native_min_value == -2.5 - assert local_temperature_calibration_entity.info_object.native_max_value == 2.5 - assert local_temperature_calibration_entity.info_object.native_step == 0.1 + assert local_temperature_calibration_entity.state.native_min_value == -2.5 + assert local_temperature_calibration_entity.state.native_max_value == 2.5 + assert local_temperature_calibration_entity.state.native_step == 0.1 assert local_temperature_calibration_entity._multiplier == 0.1 @@ -1686,7 +1686,7 @@ def discover_entities(self) -> Iterator[BaseEntity]: ] assert local_temperature_calibration_entity assert isinstance(local_temperature_calibration_entity, NumberConfigurationEntity) - assert local_temperature_calibration_entity.info_object.native_min_value == -5.0 - assert local_temperature_calibration_entity.info_object.native_max_value == 5.0 - assert local_temperature_calibration_entity.info_object.native_step == 0.1 + assert local_temperature_calibration_entity.state.native_min_value == -5.0 + assert local_temperature_calibration_entity.state.native_max_value == 5.0 + assert local_temperature_calibration_entity.state.native_step == 0.1 assert local_temperature_calibration_entity._multiplier == 0.1 diff --git a/tests/test_cover.py b/tests/test_cover.py index a9a70f597..24f594e2d 100644 --- a/tests/test_cover.py +++ b/tests/test_cover.py @@ -31,13 +31,7 @@ LIFT_MOVEMENT_TIMEOUT_RANGE, TILT_MOVEMENT_TIMEOUT_RANGE, ) -from zha.application.platforms.cover.const import ( - ATTR_CURRENT_POSITION, - ATTR_CURRENT_TILT_POSITION, - WCT, - CoverEntityFeature, - CoverState, -) +from zha.application.platforms.cover.const import WCT, CoverEntityFeature, CoverState from zha.const import STATE_CHANGED from zha.exceptions import ZHAException from zha.zigbee.device import Device @@ -45,6 +39,18 @@ Default_Response = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Default_Response].schema +def _cover_state(entity) -> CoverState | None: + """Derive the cover state from the entity's boolean state attributes.""" + state = entity.state + if state.is_opening: + return CoverState.OPENING + if state.is_closing: + return CoverState.CLOSING + if state.is_closed is None: + return None + return CoverState.CLOSED if state.is_closed else CoverState.OPEN + + ZIGPY_COVER_DEVICE = { 1: { SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, @@ -147,9 +153,9 @@ async def test_cover_non_tilt_initial_state( # pylint: disable=unused-argument ) entity = get_entity(zha_device, platform=Platform.COVER) - assert entity.state["state"] == CoverState.OPEN - assert entity.state[ATTR_CURRENT_POSITION] == 100 - assert entity.state[ATTR_CURRENT_TILT_POSITION] is None + assert _cover_state(entity) == CoverState.OPEN + assert entity.state.current_position == 100 + assert entity.state.current_tilt_position is None assert entity.supported_features == ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE @@ -161,8 +167,8 @@ async def test_cover_non_tilt_initial_state( # pylint: disable=unused-argument await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 100} ) - assert entity.state["state"] == CoverState.CLOSED - assert entity.state[ATTR_CURRENT_POSITION] == 0 + assert _cover_state(entity) == CoverState.CLOSED + assert entity.state.current_position == 0 async def test_cover_non_lift_initial_state( # pylint: disable=unused-argument @@ -190,9 +196,9 @@ async def test_cover_non_lift_initial_state( # pylint: disable=unused-argument ) entity = get_entity(zha_device, platform=Platform.COVER) - assert entity.state["state"] == CoverState.OPEN - assert entity.state[ATTR_CURRENT_POSITION] is None - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 100 + assert _cover_state(entity) == CoverState.OPEN + assert entity.state.current_position is None + assert entity.state.current_tilt_position == 100 assert entity.supported_features == ( CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT @@ -204,8 +210,8 @@ async def test_cover_non_lift_initial_state( # pylint: disable=unused-argument await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 100} ) - assert entity.state["state"] == CoverState.CLOSED - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 0 + assert _cover_state(entity) == CoverState.CLOSED + assert entity.state.current_tilt_position == 0 async def test_cover( @@ -248,31 +254,31 @@ async def test_cover( await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 100} ) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # test that the state remains after tilting to 100% (closed) await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 100} ) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # set lift to 0% (open) and test to see if state changes to open await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 0} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test that the state remains after tilting to 0% (open) await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 0} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test to see the state remains after tilting to 100% (closed) await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 100} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test entity async_update read of positions from the cluster cluster.PLUGGED_ATTR_READS[WCAttrs.current_position_lift_percentage.name] = 0 @@ -280,7 +286,7 @@ async def test_cover( update_attribute_cache(cluster) await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # close from client with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): @@ -292,17 +298,17 @@ async def test_cover( assert cluster.request.call_args[0][2].command.name == WCCmds.down_close.name assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 100} ) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # verify that a subsequent close command does not change the state to closing await entity.async_close_cover() - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # tilt close from client with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): @@ -310,7 +316,7 @@ async def test_cover( await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 0} ) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED await entity.async_close_cover_tilt() await zha_gateway.async_block_till_done() @@ -324,17 +330,17 @@ async def test_cover( assert cluster.request.call_args[0][3] == 100 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 100} ) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # verify that a subsequent close command does not change the state to closing await entity.async_close_cover_tilt() - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # open from client with patch("zigpy.zcl.Cluster.request", return_value=[0x0, zcl_f.Status.SUCCESS]): @@ -346,17 +352,17 @@ async def test_cover( assert cluster.request.call_args[0][2].command.name == WCCmds.up_open.name assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 0} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # verify that a subsequent open command does not change the state to opening await entity.async_open_cover() - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # open tilt from client with patch("zigpy.zcl.Cluster.request", return_value=[0x0, zcl_f.Status.SUCCESS]): @@ -372,21 +378,21 @@ async def test_cover( assert cluster.request.call_args[0][3] == 0 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 0} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # verify that a subsequent open command does not change the state to opening await entity.async_open_cover_tilt() - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test set position command, starting at 100 % / 0 ZCL (open) from previous lift test with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_POSITION] == 100 + assert entity.state.current_position == 100 await entity.async_set_cover_position(position=47) # 53 when inverted for ZCL await zha_gateway.async_block_till_done() assert cluster.request.call_count == 1 @@ -396,33 +402,33 @@ async def test_cover( assert cluster.request.call_args[0][3] == 53 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 35} ) - assert entity.state[ATTR_CURRENT_POSITION] == 65 - assert entity.state["state"] == CoverState.CLOSING + assert entity.state.current_position == 65 + assert _cover_state(entity) == CoverState.CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 53} ) - assert entity.state[ATTR_CURRENT_POSITION] == 47 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_position == 47 + assert _cover_state(entity) == CoverState.OPEN # verify that a subsequent go_to command does not change the state to closing/opening await entity.async_set_cover_position(position=47) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # wait for transition timeout to clear the target await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test set tilt position command, starting at 100 % / 0 ZCL (open) from previous tilt test with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 100 + assert entity.state.current_tilt_position == 100 await entity.async_set_cover_tilt_position( tilt_position=47 ) # 53 when inverted for ZCL @@ -437,29 +443,29 @@ async def test_cover( assert cluster.request.call_args[0][3] == 53 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 35} ) - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 65 - assert entity.state["state"] == CoverState.CLOSING + assert entity.state.current_tilt_position == 65 + assert _cover_state(entity) == CoverState.CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 53} ) - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 47 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_tilt_position == 47 + assert _cover_state(entity) == CoverState.OPEN # verify that a subsequent go_to command does not change the state to closing/opening await entity.async_set_cover_tilt_position(tilt_position=47) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # wait for transition timeout to clear the target await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test interrupted movement (e.g. device button press), starting from 47 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): @@ -472,20 +478,20 @@ async def test_cover( assert cluster.request.call_args[0][3] == 100 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state[ATTR_CURRENT_POSITION] == 47 - assert entity.state["state"] == CoverState.CLOSING + assert entity.state.current_position == 47 + assert _cover_state(entity) == CoverState.CLOSING # simulate a device position update to set timer to the default duration rather than dynamic await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 70} ) - assert entity.state[ATTR_CURRENT_POSITION] == 30 - assert entity.state["state"] == CoverState.CLOSING + assert entity.state.current_position == 30 + assert _cover_state(entity) == CoverState.CLOSING # wait the timer duration await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test interrupted tilt movement (e.g. device button press), starting from 47 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): @@ -500,57 +506,57 @@ async def test_cover( assert cluster.request.call_args[0][3] == 100 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 47 - assert entity.state["state"] == CoverState.CLOSING + assert entity.state.current_tilt_position == 47 + assert _cover_state(entity) == CoverState.CLOSING # simulate a device position update to set timer to the default duration rather than dynamic await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 70} ) - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 30 - assert entity.state["state"] == CoverState.CLOSING + assert entity.state.current_tilt_position == 30 + assert _cover_state(entity) == CoverState.CLOSING # wait the timer duration await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test device instigated movement (e.g. device button press), starting from 30 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_POSITION] == 30 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_position == 30 + assert _cover_state(entity) == CoverState.OPEN await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 60} ) - assert entity.state[ATTR_CURRENT_POSITION] == 40 - assert entity.state["state"] == CoverState.OPENING + assert entity.state.current_position == 40 + assert _cover_state(entity) == CoverState.OPENING # wait the default timer duration await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test device instigated tilt movement (e.g. device button press), starting from 30 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 30 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_tilt_position == 30 + assert _cover_state(entity) == CoverState.OPEN await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 60} ) - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 40 - assert entity.state["state"] == CoverState.OPENING + assert entity.state.current_tilt_position == 40 + assert _cover_state(entity) == CoverState.OPENING # wait the default timer duration await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test dynamic movement timeout, starting from 40 % and moving to 90 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_POSITION] == 40 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_position == 40 + assert _cover_state(entity) == CoverState.OPEN await entity.async_set_cover_position(position=90) # 10 when inverted for ZCL await zha_gateway.async_block_till_done() @@ -561,23 +567,23 @@ async def test_cover( assert cluster.request.call_args[0][3] == 10 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING # wait the default timer duration and verify status is still opening await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING # wait the remainder of the dynamic timeout and check if the movement timed out: (50% * 300 seconds) - default await asyncio.sleep( (50 * 0.01 * LIFT_MOVEMENT_TIMEOUT_RANGE) - DEFAULT_MOVEMENT_TIMEOUT ) - assert entity.state[ATTR_CURRENT_POSITION] == 40 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_position == 40 + assert _cover_state(entity) == CoverState.OPEN # test dynamic tilt movement timeout, starting from 40 % and moving to 90 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 40 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_tilt_position == 40 + assert _cover_state(entity) == CoverState.OPEN await entity.async_set_cover_tilt_position( tilt_position=90 @@ -590,23 +596,23 @@ async def test_cover( assert cluster.request.call_args[0][3] == 10 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING # wait the default timer duration and verify status is still opening await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING # wait the remainder of the dynamic timeout and check if the movement timed out: (50% * 30 seconds) - default await asyncio.sleep( (50 * 0.01 * TILT_MOVEMENT_TIMEOUT_RANGE) - DEFAULT_MOVEMENT_TIMEOUT ) - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 40 - assert entity.state["state"] == CoverState.OPEN + assert entity.state.current_tilt_position == 40 + assert _cover_state(entity) == CoverState.OPEN # test concurrent movement of both axis, lift and tilt starting at 40 % with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - assert entity.state[ATTR_CURRENT_POSITION] == 40 - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 40 + assert entity.state.current_position == 40 + assert entity.state.current_tilt_position == 40 await entity.async_set_cover_position(position=90) # 10 when inverted for ZCL await zha_gateway.async_block_till_done() @@ -618,7 +624,7 @@ async def test_cover( assert cluster.request.call_args[1]["expect_reply"] is True # verify the cover is opening due to the lift direction - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING await entity.async_set_cover_tilt_position( tilt_position=1 @@ -632,25 +638,25 @@ async def test_cover( assert cluster.request.call_args[1]["expect_reply"] is True # the last action's direction takes state precedence (tilt) - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING # report that tilt has reached its target await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 99} ) - assert entity.state[ATTR_CURRENT_TILT_POSITION] == 1 + assert entity.state.current_tilt_position == 1 # state should have reverted to opening because there is still an active lift target transition - assert entity.state["state"] == CoverState.OPENING + assert _cover_state(entity) == CoverState.OPENING # report that lift has reached its target await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 10} ) - assert entity.state[ATTR_CURRENT_POSITION] == 90 + assert entity.state.current_position == 90 # the state should now be open (static) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # stop from client with patch("zigpy.zcl.Cluster.request", return_value=[0x2, zcl_f.Status.SUCCESS]): @@ -729,7 +735,7 @@ async def test_cover_failures(zha_gateway: Gateway) -> None: zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 0} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # close from UI with patch( @@ -747,7 +753,7 @@ async def test_cover_failures(zha_gateway: Gateway) -> None: cluster.request.call_args[0][1] == closures.WindowCovering.ServerCommandDefs.down_close.id ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN with patch( "zigpy.zcl.Cluster.request", @@ -894,18 +900,18 @@ async def test_shade( await send_attributes_report( zha_gateway, cluster_on_off, {cluster_on_off.AttributeDefs.on_off.id: 0} ) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # test to see if it opens await send_attributes_report( zha_gateway, cluster_on_off, {cluster_on_off.AttributeDefs.on_off.id: 1} ) - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # test entity async_update await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # close from client command fails with ( @@ -923,7 +929,7 @@ async def test_shade( assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0000 - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN with patch( "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x1, zcl_f.Status.SUCCESS]) @@ -933,11 +939,11 @@ async def test_shade( assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0000 - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # open from client command fails await send_attributes_report(zha_gateway, cluster_level, {0: 0}) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED with ( patch( @@ -954,7 +960,7 @@ async def test_shade( assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0001 - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # open from client succeeds with patch( @@ -965,7 +971,7 @@ async def test_shade( assert cluster_on_off.request.call_count == 1 assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0001 - assert entity.state["state"] == CoverState.OPEN + assert _cover_state(entity) == CoverState.OPEN # set position UI command fails with ( @@ -984,7 +990,7 @@ async def test_shade( assert cluster_level.request.call_args[0][0] is False assert cluster_level.request.call_args[0][1] == 0x0004 assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 - assert entity.state[ATTR_CURRENT_POSITION] == 0 + assert entity.state.current_position == 0 # set position UI success with patch( @@ -996,11 +1002,11 @@ async def test_shade( assert cluster_level.request.call_args[0][0] is False assert cluster_level.request.call_args[0][1] == 0x0004 assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 - assert entity.state[ATTR_CURRENT_POSITION] == 47 + assert entity.state.current_position == 47 # report position change await send_attributes_report(zha_gateway, cluster_level, {8: 0, 0: 100, 1: 1}) - assert entity.state[ATTR_CURRENT_POSITION] == int(100 * 100 / 255) + assert entity.state.current_position == int(100 * 100 / 255) # stop command fails with ( @@ -1059,12 +1065,12 @@ async def test_keen_vent( # test that the state has changed from unavailable to off await send_attributes_report(zha_gateway, cluster_on_off, {8: 0, 0: False, 1: 1}) - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # test entity async_update await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # open from client command fails p1 = patch.object(cluster_on_off, "request", side_effect=asyncio.TimeoutError) @@ -1078,7 +1084,7 @@ async def test_keen_vent( assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0001 assert cluster_level.request.call_count == 1 - assert entity.state["state"] == CoverState.CLOSED + assert _cover_state(entity) == CoverState.CLOSED # open from client command success p1 = patch.object(cluster_on_off, "request", AsyncMock(return_value=[1, 0])) @@ -1091,8 +1097,8 @@ async def test_keen_vent( assert cluster_on_off.request.call_args[0][0] is False assert cluster_on_off.request.call_args[0][1] == 0x0001 assert cluster_level.request.call_count == 1 - assert entity.state["state"] == CoverState.OPEN - assert entity.state[ATTR_CURRENT_POSITION] == 100 + assert _cover_state(entity) == CoverState.OPEN + assert entity.state.current_position == 100 async def test_cover_remote(zha_gateway: Gateway) -> None: @@ -1176,15 +1182,15 @@ async def test_cover_state_restoration( ) entity = get_entity(zha_device, platform=Platform.COVER) - assert entity.state["state"] == final_state - assert entity.state[ATTR_CURRENT_POSITION] == current_position - assert entity.state[ATTR_CURRENT_TILT_POSITION] == current_tilt_position + assert _cover_state(entity) == final_state + assert entity.state.current_position == current_position + assert entity.state.current_tilt_position == current_tilt_position entity.restore_external_state_attributes(state=restore_state) if interim_state: - assert entity.state["state"] == interim_state + assert _cover_state(entity) == interim_state await asyncio.sleep(DEFAULT_MOVEMENT_TIMEOUT) - assert entity.state["state"] == final_state + assert _cover_state(entity) == final_state async def test_cover_lift_timer_cancellation_on_remove(zha_gateway: Gateway) -> None: @@ -1203,7 +1209,7 @@ async def test_cover_lift_timer_cancellation_on_remove(zha_gateway: Gateway) -> with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): await entity.async_close_cover() await zha_gateway.async_block_till_done() - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING # remove entity await entity.on_remove() @@ -1225,7 +1231,7 @@ async def test_cover_tilt_timer_cancellation_on_remove(zha_gateway: Gateway) -> with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): await entity.async_close_cover_tilt() await zha_gateway.async_block_till_done() - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING # remove entity await entity.on_remove() @@ -1247,7 +1253,7 @@ async def test_cover_state_restore_timer_cancellation_on_remove( # start state restore timer entity = get_entity(zha_device, platform=Platform.COVER) entity.restore_external_state_attributes(state=CoverState.CLOSING) - assert entity.state["state"] == CoverState.CLOSING + assert _cover_state(entity) == CoverState.CLOSING # remove entity await entity.on_remove() diff --git a/tests/test_device.py b/tests/test_device.py index 8c9703555..843db9a83 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1135,7 +1135,7 @@ async def test_quirks_v2_translation_placeholders(zha_gateway: Gateway) -> None: assert ( entity.translation_placeholders - == entity.info_object.translation_placeholders + == entity.state.translation_placeholders == {"sensor_index": "1"} ) @@ -1800,9 +1800,12 @@ async def fake_reinterview(): assert len(zha_device.platform_entities) > 0 # Verify the group is subscribed to the NEW entity, not the old one. - # Emit a state change on the new entity and check the group got it. + # Cause a real state change on the new entity and check the group got it. new_switch = get_entity(zha_device, platform=Platform.SWITCH) mock_group_entity.debounced_update.reset_mock() + new_switch._cluster.update_attribute( + general.OnOff.AttributeDefs.on_off.id, zigpy.types.Bool.true + ) new_switch.maybe_emit_state_changed_event() assert mock_group_entity.debounced_update.called diff --git a/tests/test_device_tracker.py b/tests/test_device_tracker.py index f21ab98ae..b75e92d1d 100644 --- a/tests/test_device_tracker.py +++ b/tests/test_device_tracker.py @@ -50,7 +50,7 @@ async def test_device_tracker( cluster = zigpy_device_dt.endpoints.get(1).power entity = get_entity(zha_device, platform=Platform.DEVICE_TRACKER) - assert entity.state["connected"] is False + assert entity.state.connected is False # turn state flip await send_attributes_report( @@ -63,7 +63,7 @@ async def test_device_tracker( await zha_gateway.async_block_till_done() assert entity.async_update.await_count == 1 - assert entity.state["connected"] is True + assert entity.state.connected is True assert entity.is_connected is True assert entity.source_type == SourceType.ROUTER assert entity.battery_level == 100 @@ -72,19 +72,19 @@ async def test_device_tracker( zigpy_device_dt.last_seen = time.time() - 90 await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["connected"] is False + assert entity.state.connected is False assert entity.is_connected is False # bring it back zigpy_device_dt.last_seen = time.time() await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["connected"] is True + assert entity.state.connected is True assert entity.is_connected is True # knock it offline by setting last seen None zigpy_device_dt.last_seen = None await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["connected"] is False + assert entity.state.connected is False assert entity.is_connected is False diff --git a/tests/test_discover.py b/tests/test_discover.py index edd36fb19..3c9755729 100644 --- a/tests/test_discover.py +++ b/tests/test_discover.py @@ -387,7 +387,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): qualifier_func=lambda e: e._enum == BasicCluster.PowerSource, ) assert ( - power_source_entity.state["state"] + power_source_entity.state.native_value == BasicCluster.PowerSource.Mains_single_phase.name ) @@ -397,7 +397,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): exact_entity_type=sensor.EnumSensor, qualifier_func=lambda e: e._enum == AqaraE1HookState, ) - assert hook_state_entity.state["state"] == AqaraE1HookState.Unlocked.name + assert hook_state_entity.state.native_value == AqaraE1HookState.Unlocked.name error_detected_entity = get_entity( zha_device, @@ -405,7 +405,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): exact_entity_type=binary_sensor.BinarySensor, qualifier_func=lambda e: e._attribute_name == "error_detected", ) - assert error_detected_entity.state["state"] is False + assert error_detected_entity.state.is_on is False def _get_test_device( diff --git a/tests/test_fan.py b/tests/test_fan.py index 5b13540a8..c02bedc4c 100644 --- a/tests/test_fan.py +++ b/tests/test_fan.py @@ -32,8 +32,6 @@ from zha.application.gateway import Gateway from zha.application.platforms import GroupEntity, PlatformEntity from zha.application.platforms.fan.const import ( - ATTR_PERCENTAGE, - ATTR_PRESET_MODE, PRESET_MODE_AUTO, PRESET_MODE_ON, PRESET_MODE_SMART, @@ -144,15 +142,15 @@ async def test_fan( cluster = zigpy_device.endpoints.get(1).fan entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on at fan await send_attributes_report(zha_gateway, cluster, {1: 2, 0: 1, 2: 3}) - assert entity.state["is_on"] is True + assert entity.state.is_on is True # turn off at fan await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 0, 2: 2}) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on from client cluster.write_attributes.reset_mock() @@ -161,7 +159,7 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 2}, manufacturer=UNDEFINED ) - assert entity.state["is_on"] is True + assert entity.state.is_on is True # turn off from client cluster.write_attributes.reset_mock() @@ -170,7 +168,7 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 0}, manufacturer=UNDEFINED ) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # change speed from client cluster.write_attributes.reset_mock() @@ -179,8 +177,8 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 3}, manufacturer=UNDEFINED ) - assert entity.state["is_on"] is True - assert entity.state["speed"] == SPEED_HIGH + assert entity.state.is_on is True + assert entity.state.speed == SPEED_HIGH # change preset_mode from client cluster.write_attributes.reset_mock() @@ -189,8 +187,8 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 4}, manufacturer=UNDEFINED ) - assert entity.state["is_on"] is True - assert entity.state["preset_mode"] == PRESET_MODE_ON + assert entity.state.is_on is True + assert entity.state.preset_mode == PRESET_MODE_ON # test set percentage from client cluster.write_attributes.reset_mock() @@ -201,8 +199,8 @@ async def test_fan( {"fan_mode": 2}, manufacturer=UNDEFINED ) # this is converted to a ranged value - assert entity.state["percentage"] == 66 - assert entity.state["is_on"] is True + assert entity.state.percentage == 66 + assert entity.state.is_on is True # set invalid preset_mode from client cluster.write_attributes.reset_mock() @@ -214,14 +212,14 @@ async def test_fan( # test percentage in turn on command await entity.async_turn_on(percentage=25) await zha_gateway.async_block_till_done() - assert entity.state["percentage"] == 33 # this is converted to a ranged value - assert entity.state["speed"] == SPEED_LOW + assert entity.state.percentage == 33 # this is converted to a ranged value + assert entity.state.speed == SPEED_LOW # test speed in turn on command await entity.async_turn_on(speed=SPEED_HIGH) await zha_gateway.async_block_till_done() - assert entity.state["percentage"] == 100 - assert entity.state["speed"] == SPEED_HIGH + assert entity.state.percentage == 100 + assert entity.state.speed == SPEED_HIGH async def async_turn_on( @@ -300,7 +298,7 @@ async def test_zha_group_fan_entity( assert entity.group_id == zha_group.group_id assert isinstance(entity, GroupEntity) - assert entity.info_object.fallback_name == zha_group.name + assert entity.state.fallback_name == zha_group.name group_fan_cluster = zha_group.zigpy_group.endpoint[hvac.Fan.cluster_id] @@ -308,7 +306,7 @@ async def test_zha_group_fan_entity( dev2_fan_cluster = device_fan_2.device.endpoints[1].fan # test that the fan group entity was created and is off - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on from client group_fan_cluster.write_attributes.reset_mock() @@ -352,32 +350,32 @@ async def test_zha_group_fan_entity( await send_attributes_report(zha_gateway, dev2_fan_cluster, {0: 0}) # test that group fan is off - assert entity.state["is_on"] is False + assert entity.state.is_on is False await send_attributes_report(zha_gateway, dev2_fan_cluster, {0: 2}) await zha_gateway.async_block_till_done() # no update yet because of debouncing - assert entity.state["is_on"] is False + assert entity.state.is_on is False # member updates are debounced for .5s await asyncio.sleep(1) await zha_gateway.async_block_till_done() - assert entity.state["is_on"] is True + assert entity.state.is_on is True await send_attributes_report(zha_gateway, dev2_fan_cluster, {0: 0}) await zha_gateway.async_block_till_done() # no update yet because of debouncing - assert entity.state["is_on"] is True + assert entity.state.is_on is True # member updates are debounced for .5s await asyncio.sleep(1) await zha_gateway.async_block_till_done() # test that group fan is now off - assert entity.state["is_on"] is False + assert entity.state.is_on is False await group_entity_availability_test( zha_gateway, device_fan_1, device_fan_2, entity @@ -419,7 +417,7 @@ async def test_zha_group_fan_entity_failure_state( group_fan_cluster = zha_group.zigpy_group.endpoint[hvac.Fan.cluster_id] # test that the fan group entity was created and is off - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on from client group_fan_cluster.write_attributes.reset_mock() @@ -457,10 +455,10 @@ async def test_fan_init( entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] == expected_state - assert entity.state["speed"] == expected_speed - assert entity.state["percentage"] == expected_percentage - assert entity.state["preset_mode"] is None + assert entity.state.is_on == expected_state + assert entity.state.speed == expected_speed + assert entity.state.percentage == expected_percentage + assert entity.state.preset_mode is None async def test_fan_update_entity( @@ -475,26 +473,26 @@ async def test_fan_update_entity( entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is False - assert entity.state["speed"] == SPEED_OFF - assert entity.state["percentage"] == 0 - assert entity.state["preset_mode"] is None + assert entity.state.is_on is False + assert entity.state.speed == SPEED_OFF + assert entity.state.percentage == 0 + assert entity.state.preset_mode is None assert entity.percentage_step == 100 / 3 assert cluster.read_attributes.await_count == 2 await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["is_on"] is False - assert entity.state["speed"] == SPEED_OFF + assert entity.state.is_on is False + assert entity.state.speed == SPEED_OFF assert cluster.read_attributes.await_count == 3 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["is_on"] is True - assert entity.state["percentage"] == 33 - assert entity.state["speed"] == SPEED_LOW - assert entity.state["preset_mode"] is None + assert entity.state.is_on is True + assert entity.state.percentage == 33 + assert entity.state.speed == SPEED_LOW + assert entity.state.preset_mode is None assert entity.percentage_step == 100 / 3 assert cluster.read_attributes.await_count == 4 @@ -552,15 +550,15 @@ async def test_fan_ikea( cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on at fan await send_attributes_report(zha_gateway, cluster, {"fan_mode": 1}) - assert entity.state["is_on"] is True + assert entity.state.is_on is True # turn off at fan await send_attributes_report(zha_gateway, cluster, {"fan_mode": 0}) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on from HA cluster.write_attributes.reset_mock() @@ -651,9 +649,9 @@ async def test_fan_ikea_init( zha_device = await join_zigpy_device(zha_gateway, zigpy_device_ikea) entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] == ikea_expected_state - assert entity.state["percentage"] == ikea_expected_percentage - assert entity.state["preset_mode"] == ikea_preset_mode + assert entity.state.is_on == ikea_expected_state + assert entity.state.percentage == ikea_expected_percentage + assert entity.state.preset_mode == ikea_preset_mode async def test_fan_ikea_update_entity( @@ -667,9 +665,9 @@ async def test_fan_ikea_update_entity( zha_device = await join_zigpy_device(zha_gateway, zigpy_device_ikea) entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is False - assert entity.state[ATTR_PERCENTAGE] == 0 - assert entity.state[ATTR_PRESET_MODE] is None + assert entity.state.is_on is False + assert entity.state.percentage == 0 + assert entity.state.preset_mode is None assert entity.percentage_step == 100 / 10 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1, "fan_speed": 6} @@ -677,9 +675,9 @@ async def test_fan_ikea_update_entity( await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["is_on"] is True - assert entity.state[ATTR_PERCENTAGE] == 60 - assert entity.state[ATTR_PRESET_MODE] is PRESET_MODE_AUTO + assert entity.state.is_on is True + assert entity.state.percentage == 60 + assert entity.state.preset_mode is PRESET_MODE_AUTO assert entity.percentage_step == 100 / 10 @@ -736,15 +734,15 @@ async def test_fan_kof( cluster = zigpy_device_kof.endpoints.get(1).fan entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on at fan await send_attributes_report(zha_gateway, cluster, {1: 2, 0: 1, 2: 3}) - assert entity.state["is_on"] is True + assert entity.state.is_on is True # turn off at fan await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 0, 2: 2}) - assert entity.state["is_on"] is False + assert entity.state.is_on is False # turn on from HA cluster.write_attributes.reset_mock() @@ -809,9 +807,9 @@ async def test_fan_kof_init( zha_device = await join_zigpy_device(zha_gateway, zigpy_device_kof) entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is expected_state - assert entity.state[ATTR_PERCENTAGE] == expected_percentage - assert entity.state[ATTR_PRESET_MODE] == expected_preset + assert entity.state.is_on is expected_state + assert entity.state.percentage == expected_percentage + assert entity.state.preset_mode == expected_preset async def test_fan_kof_update_entity( @@ -826,9 +824,9 @@ async def test_fan_kof_update_entity( zha_device = await join_zigpy_device(zha_gateway, zigpy_device_kof) entity = get_entity(zha_device, platform=Platform.FAN) - assert entity.state["is_on"] is False - assert entity.state[ATTR_PERCENTAGE] == 0 - assert entity.state[ATTR_PRESET_MODE] is None + assert entity.state.is_on is False + assert entity.state.percentage == 0 + assert entity.state.preset_mode is None assert entity.percentage_step == 100 / 4 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} @@ -836,7 +834,7 @@ async def test_fan_kof_update_entity( await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.state["is_on"] is True - assert entity.state[ATTR_PERCENTAGE] == 25 - assert entity.state[ATTR_PRESET_MODE] is None + assert entity.state.is_on is True + assert entity.state.percentage == 25 + assert entity.state.preset_mode is None assert entity.percentage_step == 100 / 4 diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 17792ba40..a7db0e67c 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -280,7 +280,7 @@ async def test_gateway_group_methods( entity: GroupEntity | None = get_group_entity(zha_group, platform=Platform.LIGHT) assert entity is not None - info = entity.info_object + info = entity.state assert info.class_name == "LightGroup" assert info.platform == Platform.LIGHT assert info.unique_id == "light_zha_group_0x0002" diff --git a/tests/test_light.py b/tests/test_light.py index a7dcc4419..4ffa44c0a 100644 --- a/tests/test_light.py +++ b/tests/test_light.py @@ -284,7 +284,7 @@ async def test_light_refresh( zha_device = await join_zigpy_device(zha_gateway, zigpy_device) entity = get_entity(zha_device, platform=Platform.LIGHT) - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False on_off_cluster.read_attributes.reset_mock() @@ -293,7 +293,7 @@ async def test_light_refresh( await zha_gateway.async_block_till_done() assert on_off_cluster.read_attributes.call_count == 0 assert on_off_cluster.read_attributes.await_count == 0 - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False # 1 interval - at least 1 call on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 1} @@ -301,7 +301,7 @@ async def test_light_refresh( await zha_gateway.async_block_till_done() assert on_off_cluster.read_attributes.call_count >= 1 assert on_off_cluster.read_attributes.await_count >= 1 - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # 2 intervals - at least 2 calls on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 0} @@ -309,7 +309,7 @@ async def test_light_refresh( await zha_gateway.async_block_till_done() assert on_off_cluster.read_attributes.call_count >= 2 assert on_off_cluster.read_attributes.await_count >= 2 - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False read_call_count = on_off_cluster.read_attributes.call_count read_await_count = on_off_cluster.read_attributes.await_count @@ -323,7 +323,7 @@ async def test_light_refresh( await zha_gateway.async_block_till_done() assert on_off_cluster.read_attributes.call_count == read_call_count assert on_off_cluster.read_attributes.await_count == read_await_count - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False entity.enable() @@ -333,7 +333,7 @@ async def test_light_refresh( await zha_gateway.async_block_till_done() assert on_off_cluster.read_attributes.call_count > read_call_count assert on_off_cluster.read_attributes.await_count > read_await_count - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # TODO reporting is not checked @@ -392,7 +392,7 @@ async def test_light( ) entity = get_entity(zha_device, platform=Platform.LIGHT) - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False # test turning the lights on and off from the light await async_test_on_off_from_light(zha_gateway, cluster_on_off, entity) @@ -437,14 +437,14 @@ async def test_light( if cluster_color: # test color temperature from the client with transition - assert entity.state["brightness"] != 50 - assert entity.state["color_temp"] != 200 + assert entity.state.brightness != 50 + assert entity.state.color_temp != 200 await entity.async_turn_on(brightness=50, transition=10, color_temp=200) await zha_gateway.async_block_till_done() - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP - assert entity.state["brightness"] == 50 - assert entity.state["color_temp"] == 200 - assert bool(entity.state["on"]) is True + assert entity.state.color_mode == ColorMode.COLOR_TEMP + assert entity.state.brightness == 50 + assert entity.state.color_temp == 200 + assert bool(entity.state.on) is True assert cluster_color.request.call_count == 1 assert cluster_color.request.await_count == 1 assert cluster_color.request.call_args == call( @@ -459,12 +459,12 @@ async def test_light( cluster_color.request.reset_mock() # test color xy from the client - assert entity.state["xy_color"] != [13369, 18087] + assert entity.state.xy_color != [13369, 18087] await entity.async_turn_on(brightness=50, xy_color=[13369, 18087]) await zha_gateway.async_block_till_done() - assert entity.state["color_mode"] == ColorMode.XY - assert entity.state["brightness"] == 50 - assert entity.state["xy_color"] == [13369, 18087] + assert entity.state.color_mode == ColorMode.XY + assert entity.state.brightness == 50 + assert entity.state.xy_color == [13369, 18087] assert cluster_color.request.call_count == 1 assert cluster_color.request.await_count == 1 assert cluster_color.request.call_args == call( @@ -493,11 +493,11 @@ async def async_test_on_off_from_light( # group member updates are debounced if isinstance(entity, GroupEntity): - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # turn off at light await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 0, 2: 3}) @@ -505,11 +505,11 @@ async def async_test_on_off_from_light( # group member updates are debounced if isinstance(entity, GroupEntity): - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False async def async_test_on_from_light( @@ -526,11 +526,11 @@ async def async_test_on_from_light( # group member updates are debounced if isinstance(entity, GroupEntity): - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True async def async_test_on_off_from_client( @@ -543,7 +543,7 @@ async def async_test_on_off_from_client( cluster.request.reset_mock() await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( @@ -568,7 +568,7 @@ async def async_test_off_from_client( cluster.request.reset_mock() await entity.async_turn_off() await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( @@ -601,7 +601,7 @@ async def _reset_light(): await zha_gateway.async_block_till_done() on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await _reset_light() await _async_shift_time(zha_gateway) @@ -609,7 +609,7 @@ async def _reset_light(): # turn on via UI await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True assert on_off_cluster.request.call_count == 1 assert on_off_cluster.request.await_count == 1 assert level_cluster.request.call_count == 0 @@ -627,7 +627,7 @@ async def _reset_light(): await entity.async_turn_on(transition=10) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True assert on_off_cluster.request.call_count == 0 assert on_off_cluster.request.await_count == 0 assert level_cluster.request.call_count == 1 @@ -646,7 +646,7 @@ async def _reset_light(): await entity.async_turn_on(brightness=10) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # the onoff cluster is now not used when brightness is present by default assert on_off_cluster.request.call_count == 0 assert on_off_cluster.request.await_count == 0 @@ -680,16 +680,16 @@ async def async_test_dimmer_from_light( zha_gateway, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} ) await zha_gateway.async_block_till_done() - assert entity.state["on"] == expected_state + assert entity.state.on == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: - assert entity.state["brightness"] is None + assert entity.state.brightness is None else: # group member updates are debounced if isinstance(entity, GroupEntity): await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert entity.state["brightness"] == level + assert entity.state.brightness == level async def async_test_flash_from_client( @@ -703,7 +703,7 @@ async def async_test_flash_from_client( cluster.request.reset_mock() await entity.async_turn_on(flash=flash) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True assert cluster.request.call_count == 1 assert cluster.request.await_count == 1 assert cluster.request.call_args == call( @@ -762,7 +762,7 @@ async def test_zha_group_light_entity( entity: GroupEntity = get_group_entity(zha_group, platform=Platform.LIGHT) assert entity.group_id == zha_group.group_id - assert entity.info_object.fallback_name == zha_group.name + assert entity.state.fallback_name == zha_group.name device_1_light_entity = get_entity(device_light_1, platform=Platform.LIGHT) device_2_light_entity = get_entity(device_light_2, platform=Platform.LIGHT) @@ -789,7 +789,7 @@ async def test_zha_group_light_entity( dev1_cluster_level = device_light_1.device.endpoints[1].level # test that the lights were created and are off - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False # Group entities do not support state restoration, # except for off_brightness and off_with_transition @@ -804,9 +804,9 @@ async def test_zha_group_light_entity( effect="colorloop", ) - assert bool(entity.state["on"]) is False - assert bool(entity.state["off_with_transition"]) is False - assert entity.state["off_brightness"] == 12 + assert bool(entity.state.on) is False + assert bool(entity.state.off_with_transition) is False + assert entity.state.off_brightness == 12 # test turning the lights on and off from the client await async_test_on_off_from_client(zha_gateway, group_cluster_on_off, entity) @@ -850,42 +850,42 @@ async def test_zha_group_light_entity( await zha_gateway.async_block_till_done() # test that group light is on - assert device_1_light_entity.state["on"] is True - assert device_2_light_entity.state["on"] is True - assert bool(entity.state["on"]) is True + assert device_1_light_entity.state.on is True + assert device_2_light_entity.state.on is True + assert bool(entity.state.on) is True await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() # test that group light is still on - assert device_1_light_entity.state["on"] is False - assert device_2_light_entity.state["on"] is True - assert bool(entity.state["on"]) is True + assert device_1_light_entity.state.on is False + assert device_2_light_entity.state.on is True + assert bool(entity.state.on) is True await send_attributes_report(zha_gateway, dev2_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() # test that group light is now off - assert device_1_light_entity.state["on"] is False - assert device_2_light_entity.state["on"] is False + assert device_1_light_entity.state.on is False + assert device_2_light_entity.state.on is False # group member updates are debounced - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 1}) await zha_gateway.async_block_till_done() # test that group light is now back on - assert device_1_light_entity.state["on"] is True - assert device_2_light_entity.state["on"] is False + assert device_1_light_entity.state.on is True + assert device_2_light_entity.state.on is False # group member updates are debounced - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True await group_entity_availability_test( zha_gateway, device_light_1, device_light_2, entity @@ -894,13 +894,13 @@ async def test_zha_group_light_entity( # turn it off to test a new member add being tracked await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() - assert device_1_light_entity.state["on"] is False - assert device_2_light_entity.state["on"] is False + assert device_1_light_entity.state.on is False + assert device_2_light_entity.state.on is False # group member updates are debounced - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False # add a new member and test that his state is also tracked await zha_group.async_add_members( @@ -914,14 +914,14 @@ async def test_zha_group_light_entity( await send_attributes_report(zha_gateway, dev3_cluster_on_off, {0: 1}) await zha_gateway.async_block_till_done() - assert device_1_light_entity.state["on"] is False - assert device_2_light_entity.state["on"] is False - assert device_3_light_entity.state["on"] is True + assert device_1_light_entity.state.on is False + assert device_2_light_entity.state.on is False + assert device_3_light_entity.state.on is True # group member updates are debounced - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # make the group have only 1 member and now there should be no entity await zha_group.async_remove_members( @@ -950,7 +950,7 @@ async def test_zha_group_light_entity( assert entity is not None await send_attributes_report(zha_gateway, dev3_cluster_on_off, {0: 1}) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # add a 3rd member and ensure we still have an entity and we track the new member # First we turn the lights currently in the group off @@ -958,10 +958,10 @@ async def test_zha_group_light_entity( await send_attributes_report(zha_gateway, dev3_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() # group member updates are debounced - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False # this will test that _reprobe_group is used correctly await zha_group.async_add_members( @@ -977,10 +977,10 @@ async def test_zha_group_light_entity( await send_attributes_report(zha_gateway, dev2_cluster_on_off, {0: 1}) await zha_gateway.async_block_till_done() # group member updates are debounced - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False await asyncio.sleep(0.1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True await zha_group.async_remove_members( [GroupMemberReference(ieee=coordinator.ieee, endpoint_id=1)] @@ -988,7 +988,7 @@ async def test_zha_group_light_entity( await zha_gateway.async_block_till_done() entity = get_group_entity(zha_group, platform=Platform.LIGHT) assert entity is not None - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True assert len(zha_group.members) == 3 # remove the group and ensure that there is no entity and that the entity registry is cleaned up @@ -1142,9 +1142,9 @@ async def test_transitions( eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color # test that the lights were created and are off - assert bool(entity.state["on"]) is False - assert bool(device_1_light_entity.state["on"]) is False - assert bool(device_2_light_entity.state["on"]) is False + assert bool(entity.state.on) is False + assert bool(device_1_light_entity.state.on) is False + assert bool(device_2_light_entity.state.on) is False # first test 0 length transition with no color and no brightness provided dev1_cluster_on_off.request.reset_mock() @@ -1167,8 +1167,8 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 254 + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 254 # test 0 length transition with no color and no brightness provided again, but for "force on" lights eWeLink_cluster_on_off.request.reset_mock() @@ -1199,8 +1199,8 @@ async def test_transitions( manufacturer=None, ) - assert bool(eWeLink_light_entity.state["on"]) is True - assert eWeLink_light_entity.state["brightness"] == 254 + assert bool(eWeLink_light_entity.state.on) is True + assert eWeLink_light_entity.state.brightness == 254 eWeLink_cluster_on_off.request.reset_mock() eWeLink_cluster_level.request.reset_mock() @@ -1226,8 +1226,8 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 50 + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 50 dev1_cluster_level.request.reset_mock() @@ -1261,10 +1261,10 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 18 - assert device_1_light_entity.state["color_temp"] == 432 - assert device_1_light_entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 18 + assert device_1_light_entity.state.color_temp == 432 + assert device_1_light_entity.state.color_mode == ColorMode.COLOR_TEMP dev1_cluster_level.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1288,7 +1288,7 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is False + assert bool(device_1_light_entity.state.on) is False dev1_cluster_level.request.reset_mock() @@ -1333,10 +1333,10 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 25 - assert device_1_light_entity.state["color_temp"] == 235 - assert device_1_light_entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 25 + assert device_1_light_entity.state.color_temp == 235 + assert device_1_light_entity.state.color_mode == ColorMode.COLOR_TEMP dev1_cluster_level.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1351,7 +1351,7 @@ async def test_transitions( assert dev1_cluster_level.request.call_count == 0 assert dev1_cluster_level.request.await_count == 0 - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1396,10 +1396,10 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 25 - assert device_1_light_entity.state["color_temp"] == 236 - assert device_1_light_entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 25 + assert device_1_light_entity.state.color_temp == 236 + assert device_1_light_entity.state.color_mode == ColorMode.COLOR_TEMP dev1_cluster_level.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1413,7 +1413,7 @@ async def test_transitions( assert dev1_cluster_color.request.await_count == 0 assert dev1_cluster_level.request.call_count == 0 assert dev1_cluster_level.request.await_count == 0 - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1447,10 +1447,10 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 25 - assert device_1_light_entity.state["color_temp"] == 236 - assert device_1_light_entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 25 + assert device_1_light_entity.state.color_temp == 236 + assert device_1_light_entity.state.color_mode == ColorMode.COLOR_TEMP dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1464,7 +1464,7 @@ async def test_transitions( assert dev1_cluster_color.request.await_count == 0 assert dev1_cluster_level.request.call_count == 0 assert dev1_cluster_level.request.await_count == 0 - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1493,8 +1493,8 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_2_light_entity.state["on"]) is True - assert device_2_light_entity.state["brightness"] == 100 + assert bool(device_2_light_entity.state.on) is True + assert device_2_light_entity.state.brightness == 100 dev2_cluster_level.request.reset_mock() @@ -1507,7 +1507,7 @@ async def test_transitions( assert dev2_cluster_color.request.await_count == 0 assert dev2_cluster_level.request.call_count == 0 assert dev2_cluster_level.request.await_count == 0 - assert bool(device_2_light_entity.state["on"]) is False + assert bool(device_2_light_entity.state.on) is False dev2_cluster_on_off.request.reset_mock() @@ -1552,10 +1552,10 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_2_light_entity.state["on"]) is True - assert device_2_light_entity.state["brightness"] == 25 - assert device_2_light_entity.state["color_temp"] == 235 - assert device_2_light_entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(device_2_light_entity.state.on) is True + assert device_2_light_entity.state.brightness == 25 + assert device_2_light_entity.state.color_temp == 235 + assert device_2_light_entity.state.color_mode == ColorMode.COLOR_TEMP dev2_cluster_level.request.reset_mock() dev2_cluster_color.request.reset_mock() @@ -1569,7 +1569,7 @@ async def test_transitions( assert dev2_cluster_color.request.await_count == 0 assert dev2_cluster_level.request.call_count == 0 assert dev2_cluster_level.request.await_count == 0 - assert bool(device_2_light_entity.state["on"]) is False + assert bool(device_2_light_entity.state.on) is False dev2_cluster_on_off.request.reset_mock() @@ -1607,10 +1607,10 @@ async def test_transitions( manufacturer=None, ) - assert bool(entity.state["on"]) is True - assert entity.state["brightness"] == 25 - assert entity.state["color_temp"] == 235 - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(entity.state.on) is True + assert entity.state.brightness == 25 + assert entity.state.color_temp == 235 + assert entity.state.color_mode == ColorMode.COLOR_TEMP group_on_off_cluster.request.reset_mock() group_color_cluster.request.reset_mock() @@ -1625,7 +1625,7 @@ async def test_transitions( assert dev2_cluster_color.request.await_count == 0 assert dev2_cluster_level.request.call_count == 0 assert dev2_cluster_level.request.await_count == 0 - assert bool(device_2_light_entity.state["on"]) is True + assert bool(device_2_light_entity.state.on) is True dev2_cluster_on_off.request.reset_mock() @@ -1648,7 +1648,7 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_2_light_entity.state["on"]) is False + assert bool(device_2_light_entity.state.on) is False dev2_cluster_level.request.reset_mock() @@ -1671,7 +1671,7 @@ async def test_transitions( manufacturer=None, ) - assert bool(device_2_light_entity.state["on"]) is True + assert bool(device_2_light_entity.state.on) is True dev2_cluster_level.request.reset_mock() eWeLink_cluster_on_off.request.reset_mock() @@ -1706,9 +1706,9 @@ async def test_transitions( manufacturer=None, ) - assert bool(eWeLink_light_entity.state["on"]) is True - assert eWeLink_light_entity.state["color_temp"] == 235 - assert eWeLink_light_entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(eWeLink_light_entity.state.on) is True + assert eWeLink_light_entity.state.color_temp == 235 + assert eWeLink_light_entity.state.color_mode == ColorMode.COLOR_TEMP assert eWeLink_light_entity.min_mireds == 153 assert eWeLink_light_entity.max_mireds == 500 @@ -1771,9 +1771,9 @@ async def test_on_with_off_color(zha_gateway: Gateway) -> None: manufacturer=None, ) - assert bool(entity.state["on"]) is True - assert entity.state["color_temp"] == 235 - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(entity.state.on) is True + assert entity.state.color_temp == 235 + assert entity.state.color_mode == ColorMode.COLOR_TEMP assert entity.supported_color_modes == {ColorMode.COLOR_TEMP, ColorMode.XY} assert entity._internal_supported_color_modes == { ColorMode.COLOR_TEMP, @@ -1832,10 +1832,10 @@ async def test_on_with_off_color(zha_gateway: Gateway) -> None: manufacturer=None, ) - assert bool(entity.state["on"]) is True - assert entity.state["color_temp"] == 240 - assert entity.state["brightness"] == 254 - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert bool(entity.state.on) is True + assert entity.state.color_temp == 240 + assert entity.state.brightness == 254 + assert entity.state.color_mode == ColorMode.COLOR_TEMP @patch( @@ -1886,7 +1886,7 @@ async def test_group_member_assume_state(zha_gateway: Gateway) -> None: group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] # test that the lights were created and are off - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False group_cluster_on_off.request.reset_mock() await asyncio.sleep(11) @@ -1897,9 +1897,9 @@ async def test_group_member_assume_state(zha_gateway: Gateway) -> None: await asyncio.sleep(1) # wait for assume debounce # members also instantly assume STATE_ON - assert bool(device_1_light_entity.state["on"]) is True - assert bool(device_2_light_entity.state["on"]) is True - assert bool(entity.state["on"]) is True + assert bool(device_1_light_entity.state.on) is True + assert bool(device_2_light_entity.state.on) is True + assert bool(entity.state.on) is True # turn off via UI await entity.async_turn_off() @@ -1907,22 +1907,22 @@ async def test_group_member_assume_state(zha_gateway: Gateway) -> None: await asyncio.sleep(1) # members also instantly assume STATE_OFF - assert bool(device_1_light_entity.state["on"]) is False - assert bool(device_2_light_entity.state["on"]) is False - assert bool(entity.state["on"]) is False + assert bool(device_1_light_entity.state.on) is False + assert bool(device_2_light_entity.state.on) is False + assert bool(entity.state.on) is False # now test members with different state not being overridden # turn on light 1 to brightness 50 await device_1_light_entity.async_turn_on(brightness=50) await zha_gateway.async_block_till_done() - assert bool(device_1_light_entity.state["on"]) is True - assert device_1_light_entity.state["brightness"] == 50 + assert bool(device_1_light_entity.state.on) is True + assert device_1_light_entity.state.brightness == 50 # turn on light 2 to brightness 100 await device_2_light_entity.async_turn_on(brightness=100) await zha_gateway.async_block_till_done() - assert bool(device_2_light_entity.state["on"]) is True - assert device_2_light_entity.state["brightness"] == 100 + assert bool(device_2_light_entity.state.on) is True + assert device_2_light_entity.state.brightness == 100 await asyncio.sleep(1) # wait for assume debounce @@ -1931,11 +1931,11 @@ async def test_group_member_assume_state(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() await asyncio.sleep(1) - assert entity.state["brightness"] == 75 # average + assert entity.state.brightness == 75 # average # but members do not change unchanged state - assert device_1_light_entity.state["brightness"] == 50 - assert device_2_light_entity.state["brightness"] == 100 + assert device_1_light_entity.state.brightness == 50 + assert device_2_light_entity.state.brightness == 100 @patch( @@ -1956,15 +1956,15 @@ async def test_transition_brightness_buffering(zha_gateway: Gateway) -> None: dev1_cluster_level = device_light_1.device.endpoints[1].level entity = get_entity(device_light_1, platform=Platform.LIGHT) - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False # Turn on with a short transition and a target brightness of 200. await entity.async_turn_on(transition=0.1, brightness=200) await zha_gateway.async_block_till_done() # The state is optimistically set to the target brightness immediately. - assert bool(entity.state["on"]) is True - assert entity.state["brightness"] == 200 + assert bool(entity.state.on) is True + assert entity.state.brightness == 200 assert entity.is_transitioning # Simulate intermediate brightness reports during the transition (light slowly ramping up). @@ -1975,7 +1975,7 @@ async def test_transition_brightness_buffering(zha_gateway: Gateway) -> None: {general.LevelControl.AttributeDefs.current_level.id: 50}, ) await zha_gateway.async_block_till_done() - assert entity.state["brightness"] == 200 # still the optimistic value + assert entity.state.brightness == 200 # still the optimistic value # The light only goes to brightness 120 (for some reason), not the requested 200. await send_attributes_report( @@ -1984,7 +1984,7 @@ async def test_transition_brightness_buffering(zha_gateway: Gateway) -> None: {general.LevelControl.AttributeDefs.current_level.id: 120}, ) await zha_gateway.async_block_till_done() - assert entity.state["brightness"] == 200 # still buffered, not yet applied + assert entity.state.brightness == 200 # still buffered, not yet applied # Wait for the transition timer to fire (0.1 + 0.5s delay = 0.6s). await asyncio.sleep(0.8) @@ -1992,14 +1992,14 @@ async def test_transition_brightness_buffering(zha_gateway: Gateway) -> None: # After the transition, the last buffered report (120) is applied instead of the target (200). assert not entity.is_transitioning - assert entity.state["brightness"] == 120 + assert entity.state.brightness == 120 # Now verify that if no brightness reports arrive during a transition, the # optimistically set target brightness is preserved unchanged. await entity.async_turn_on(transition=0.1, brightness=150) await zha_gateway.async_block_till_done() - assert entity.state["brightness"] == 150 + assert entity.state.brightness == 150 assert entity.is_transitioning # No level reports during this transition. @@ -2007,7 +2007,7 @@ async def test_transition_brightness_buffering(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() assert not entity.is_transitioning - assert entity.state["brightness"] == 150 # target preserved + assert entity.state.brightness == 150 # target preserved @patch( @@ -2033,12 +2033,12 @@ async def test_turn_on_during_off_transition(zha_gateway: Gateway) -> None: # Start with the light on. await entity.async_turn_on(brightness=200) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # Turn it off with a transition (timer runs for 1 + 0.5s = 1.5s). await entity.async_turn_off(transition=1) await zha_gateway.async_block_till_done() - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False assert entity.is_transitioning # Before the off-transition timer fires, turn the light back on. @@ -2046,8 +2046,8 @@ async def test_turn_on_during_off_transition(zha_gateway: Gateway) -> None: await entity.async_turn_on(brightness=150) await zha_gateway.async_block_till_done() # Optimistically, HA now thinks it's on. - assert bool(entity.state["on"]) is True - assert entity.state["brightness"] == 150 + assert bool(entity.state.on) is True + assert entity.state.brightness == 150 assert entity.is_transitioning # The device correctly reports it is still off (it refused the on command). @@ -2060,13 +2060,13 @@ async def test_turn_on_during_off_transition(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() # During the transition window the state is still optimistically on. - assert bool(entity.state["on"]) is True + assert bool(entity.state.on) is True # Once the transition timer fires, the buffered off report is applied. await asyncio.sleep(0.8) await zha_gateway.async_block_till_done() assert not entity.is_transitioning - assert bool(entity.state["on"]) is False + assert bool(entity.state.on) is False async def test_light_state_restoration(zha_gateway: Gateway) -> None: @@ -2084,12 +2084,12 @@ async def test_light_state_restoration(zha_gateway: Gateway) -> None: effect="colorloop", ) - assert entity.state["on"] is True - assert entity.state["brightness"] == 34 - assert entity.state["color_temp"] == 500 - assert entity.state["xy_color"] == (1, 2) - assert entity.state["color_mode"] == ColorMode.XY - assert entity.state["effect"] == "colorloop" + assert entity.state.on is True + assert entity.state.brightness == 34 + assert entity.state.color_temp == 500 + assert entity.state.xy_color == (1, 2) + assert entity.state.color_mode == ColorMode.XY + assert entity.state.effect == "colorloop" entity.restore_external_state_attributes( state=None, @@ -2102,12 +2102,12 @@ async def test_light_state_restoration(zha_gateway: Gateway) -> None: effect=None, ) - assert entity.state["on"] is True - assert entity.state["brightness"] == 34 - assert entity.state["color_temp"] == 500 - assert entity.state["xy_color"] == (1, 2) - assert entity.state["color_mode"] == ColorMode.XY - assert entity.state["effect"] == "colorloop" + assert entity.state.on is True + assert entity.state.brightness == 34 + assert entity.state.color_temp == 500 + assert entity.state.xy_color == (1, 2) + assert entity.state.color_mode == ColorMode.XY + assert entity.state.effect == "colorloop" async def test_light_state_restoration_unsupported_color_mode( @@ -2127,7 +2127,7 @@ async def test_light_state_restoration_unsupported_color_mode( entity = get_entity(zha_device, platform=Platform.LIGHT) assert entity.supported_color_modes == {ColorMode.COLOR_TEMP} - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP + assert entity.state.color_mode == ColorMode.COLOR_TEMP # Attempt to restore XY color_mode on a color_temp-only light entity.restore_external_state_attributes( @@ -2142,8 +2142,8 @@ async def test_light_state_restoration_unsupported_color_mode( ) # color_mode should remain COLOR_TEMP since XY is not supported - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP - assert entity.state["color_temp"] == 300 + assert entity.state.color_mode == ColorMode.COLOR_TEMP + assert entity.state.color_temp == 300 async def test_color_temp_only_light_ignores_incorrect_color_mode( @@ -2163,8 +2163,8 @@ async def test_color_temp_only_light_ignores_incorrect_color_mode( entity = get_entity(zha_device, platform=Platform.LIGHT) assert entity.supported_color_modes == {ColorMode.COLOR_TEMP} - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP - assert entity.state["color_temp"] == 250 + assert entity.state.color_mode == ColorMode.COLOR_TEMP + assert entity.state.color_temp == 250 # Simulate the device incorrectly reporting XY color mode during a poll color_cluster.PLUGGED_ATTR_READS = { @@ -2180,9 +2180,9 @@ async def test_color_temp_only_light_ignores_incorrect_color_mode( await entity.async_update() # color_mode should remain COLOR_TEMP and color_temp should be updated - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP - assert entity.state["color_temp"] == 300 - assert entity.state["xy_color"] is None + assert entity.state.color_mode == ColorMode.COLOR_TEMP + assert entity.state.color_temp == 300 + assert entity.state.xy_color is None # Same test with Hue_and_saturation mode color_cluster.PLUGGED_ATTR_READS = { @@ -2196,9 +2196,9 @@ async def test_color_temp_only_light_ignores_incorrect_color_mode( await entity.async_update() - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP - assert entity.state["color_temp"] == 400 - assert entity.state["xy_color"] is None + assert entity.state.color_mode == ColorMode.COLOR_TEMP + assert entity.state.color_temp == 400 + assert entity.state.xy_color is None async def test_poll_updates_color_mode_on_dual_mode_light( @@ -2225,9 +2225,9 @@ async def test_poll_updates_color_mode_on_dual_mode_light( update_attribute_cache(color_cluster) await entity.async_update() - assert entity.state["color_mode"] == ColorMode.COLOR_TEMP - assert entity.state["color_temp"] == 350 - assert entity.state["xy_color"] is None + assert entity.state.color_mode == ColorMode.COLOR_TEMP + assert entity.state.color_temp == 350 + assert entity.state.xy_color is None # Poll with XY mode color_cluster.PLUGGED_ATTR_READS = { @@ -2243,9 +2243,9 @@ async def test_poll_updates_color_mode_on_dual_mode_light( update_attribute_cache(color_cluster) await entity.async_update() - assert entity.state["color_mode"] == ColorMode.XY - assert entity.state["xy_color"] == (30000 / 65535, 25000 / 65535) - assert entity.state["color_temp"] is None + assert entity.state.color_mode == ColorMode.XY + assert entity.state.xy_color == (30000 / 65535, 25000 / 65535) + assert entity.state.color_temp is None async def test_turn_on_cancellation_cleans_up_transition_flag( diff --git a/tests/test_lock.py b/tests/test_lock.py index 570e77863..a4e29f341 100644 --- a/tests/test_lock.py +++ b/tests/test_lock.py @@ -47,15 +47,15 @@ async def test_lock(zha_gateway: Gateway) -> None: cluster = zigpy_device.endpoints[1].door_lock entity = get_entity(zha_device, platform=Platform.LOCK) - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False # set state to locked await send_attributes_report(zha_gateway, cluster, {1: 0, 0: 1, 2: 2}) - assert entity.state["is_locked"] is True + assert entity.state.is_locked is True # set state to unlocked await send_attributes_report(zha_gateway, cluster, {1: 0, 0: 2, 2: 3}) - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False # lock from HA await async_lock(zha_gateway, cluster, entity) @@ -77,13 +77,13 @@ async def test_lock(zha_gateway: Gateway) -> None: # test updating entity state from client cluster.read_attributes.reset_mock() - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False cluster.PLUGGED_ATTR_READS = {"lock_state": 1} update_attribute_cache(cluster) await entity.async_update() await zha_gateway.async_block_till_done() assert cluster.read_attributes.call_count == 1 - assert entity.state["is_locked"] is True + assert entity.state.is_locked is True async def async_lock( @@ -95,7 +95,7 @@ async def async_lock( with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): await entity.async_lock() await zha_gateway.async_block_till_done() - assert entity.state["is_locked"] is True + assert entity.state.is_locked is True assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == LOCK_DOOR @@ -105,7 +105,7 @@ async def async_lock( with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.FAILURE]): await entity.async_unlock() await zha_gateway.async_block_till_done() - assert entity.state["is_locked"] is True + assert entity.state.is_locked is True assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == UNLOCK_DOOR @@ -121,7 +121,7 @@ async def async_unlock( with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): await entity.async_unlock() await zha_gateway.async_block_till_done() - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == UNLOCK_DOOR @@ -131,7 +131,7 @@ async def async_unlock( with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.FAILURE]): await entity.async_lock() await zha_gateway.async_block_till_done() - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False assert cluster.request.call_args[0][1] == LOCK_DOOR @@ -212,10 +212,10 @@ async def test_lock_state_restoration(zha_gateway: Gateway) -> None: entity = get_entity(zha_device, platform=Platform.LOCK) - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False entity.restore_external_state_attributes(state=STATE_LOCKED) - assert entity.state["is_locked"] is True + assert entity.state.is_locked is True entity.restore_external_state_attributes(state=STATE_UNLOCKED) - assert entity.state["is_locked"] is False + assert entity.state.is_locked is False diff --git a/tests/test_number.py b/tests/test_number.py index e49478d27..2fc9641ef 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -123,13 +123,13 @@ async def test_number( assert entity.fallback_name == "PWM1" # test that the state is 15.0 - assert entity.state["state"] == 15.0 + assert entity.state.native_value == 15.0 # test attributes - assert entity.info_object.native_min_value == 1.0 - assert entity.info_object.native_max_value == 100.0 - assert entity.info_object.native_step == 1.1 - assert entity.info_object.mode == NumberMode.AUTO + assert entity.state.native_min_value == 1.0 + assert entity.state.native_max_value == 100.0 + assert entity.state.native_step == 1.1 + assert entity.state.mode == NumberMode.AUTO assert entity.icon == "mdi:percent" assert entity.native_unit_of_measurement == "%" @@ -141,12 +141,12 @@ async def test_number( assert cluster.read_attributes.call_count == 2 await send_attributes_report(zha_gateway, cluster, {0x0055: 15}) await zha_gateway.async_block_till_done() - assert entity.state["state"] == 15.0 + assert entity.state.native_value == 15.0 # update value from device await send_attributes_report(zha_gateway, cluster, {0x0055: 20}) await zha_gateway.async_block_till_done() - assert entity.state["state"] == 20.0 + assert entity.state.native_value == 20.0 # change value from client await entity.async_set_native_value(30.0) @@ -156,11 +156,11 @@ async def test_number( assert cluster.write_attributes.call_args == call( {"present_value": 30.0}, manufacturer=UNDEFINED ) - assert entity.state["state"] == 30.0 + assert entity.state.native_value == 30.0 # test updating entity state from client cluster.read_attributes.reset_mock() - assert entity.state["state"] == 30.0 + assert entity.state.native_value == 30.0 cluster.PLUGGED_ATTR_READS = {"present_value": 20} await entity.async_update() await zha_gateway.async_block_till_done() @@ -168,7 +168,7 @@ async def test_number( assert cluster.read_attributes.await_args == call( ["present_value"], allow_cache=False, only_cache=False, manufacturer=UNDEFINED ) - assert entity.state["state"] == 20.0 + assert entity.state.native_value == 20.0 await entity.async_set_native_value(30) await zha_gateway.async_block_till_done() @@ -176,7 +176,7 @@ async def test_number( assert cluster.write_attributes.call_args == call( {"present_value": 30}, manufacturer=UNDEFINED ) - assert entity.state["state"] == 30.0 + assert entity.state.native_value == 30.0 async def test_number_missing_description_attr( @@ -258,7 +258,7 @@ async def test_level_control_number( ), ] - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value assert entity._attr_entity_category == EntityCategory.CONFIG assert entity.icon is None @@ -274,12 +274,12 @@ async def test_level_control_number( call({attr: new_value}, manufacturer=UNDEFINED) ] - assert entity.state["state"] == new_value + assert entity.state.native_value == new_value level_control_cluster.read_attributes.reset_mock() await entity.async_update() # the mocking doesn't update the attr cache so this flips back to initial value - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value assert level_control_cluster.read_attributes.mock_calls == [ call( [attr], @@ -298,11 +298,11 @@ async def test_level_control_number( assert level_control_cluster.write_attributes.mock_calls == [ call({attr: new_value}, manufacturer=UNDEFINED), ] - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value # test updating entity state from client level_control_cluster.read_attributes.reset_mock() - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value level_control_cluster.PLUGGED_ATTR_READS = {attr: new_value} await entity.async_update() await zha_gateway.async_block_till_done() @@ -317,7 +317,7 @@ async def test_level_control_number( manufacturer=UNDEFINED, ), ] - assert entity.state["state"] == new_value + assert entity.state.native_value == new_value # update value from device await send_attributes_report( @@ -326,7 +326,7 @@ async def test_level_control_number( {level_control_cluster.attributes_by_name[attr].id: initial_value}, ) await zha_gateway.async_block_till_done() - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value @pytest.mark.parametrize( @@ -366,7 +366,7 @@ async def test_color_number( in color_cluster.read_attributes.call_args_list ) - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value assert entity._attr_entity_category == EntityCategory.CONFIG await entity.async_set_native_value(new_value) @@ -375,12 +375,12 @@ async def test_color_number( attr: new_value, } - assert entity.state["state"] == new_value + assert entity.state.native_value == new_value color_cluster.read_attributes.reset_mock() await entity.async_update() # the mocking doesn't update the attr cache so this flips back to initial value - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value assert color_cluster.read_attributes.call_count == 1 assert ( call( @@ -401,11 +401,11 @@ async def test_color_number( assert color_cluster.write_attributes.mock_calls == [ call({attr: new_value}, manufacturer=UNDEFINED), ] - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value # test updating entity state from client color_cluster.read_attributes.reset_mock() - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value color_cluster.PLUGGED_ATTR_READS = {attr: new_value} await entity.async_update() await zha_gateway.async_block_till_done() @@ -420,7 +420,7 @@ async def test_color_number( manufacturer=UNDEFINED, ), ] - assert entity.state["state"] == new_value + assert entity.state.native_value == new_value # update value from device await send_attributes_report( @@ -429,4 +429,4 @@ async def test_color_number( {color_cluster.attributes_by_name[attr].id: initial_value}, ) await zha_gateway.async_block_till_done() - assert entity.state["state"] == initial_value + assert entity.state.native_value == initial_value diff --git a/tests/test_select.py b/tests/test_select.py index 9e732f244..f1b7dbc12 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -61,8 +61,8 @@ async def test_select(zha_gateway: Gateway) -> None: select_name = security.IasWd.Warning.WarningMode.__name__ entity = get_entity(zha_device, platform=Platform.SELECT, qualifier=select_name) - assert entity.state["state"] is None # unknown in HA - assert entity.info_object.options == [ + assert entity.state.current_option is None # unknown in HA + assert entity.state.options == [ "Stop", "Burglar", "Fire", @@ -76,7 +76,9 @@ async def test_select(zha_gateway: Gateway) -> None: # change value from client await entity.async_select_option(security.IasWd.Warning.WarningMode.Burglar.name) await zha_gateway.async_block_till_done() - assert entity.state["state"] == security.IasWd.Warning.WarningMode.Burglar.name + assert ( + entity.state.current_option == security.IasWd.Warning.WarningMode.Burglar.name + ) class MotionSensitivityQuirk(CustomDevice): @@ -137,13 +139,13 @@ async def test_on_off_select_attribute_report(zha_gateway: Gateway) -> None: cluster = aqara_sensor.device.endpoints.get(1).opple_cluster entity = get_entity(aqara_sensor, platform=Platform.SELECT) - assert entity.state["state"] == AqaraMotionSensitivities.Medium.name + assert entity.state.current_option == AqaraMotionSensitivities.Medium.name # send attribute report from device await send_attributes_report( zha_gateway, cluster, {"motion_sensitivity": AqaraMotionSensitivities.Low} ) - assert entity.state["state"] == AqaraMotionSensitivities.Low.name + assert entity.state.current_option == AqaraMotionSensitivities.Low.name ( @@ -206,17 +208,17 @@ async def test_on_off_select_attribute_report_v2( entity = get_entity( zha_device, platform=Platform.SELECT, - qualifier_func=lambda e: e.info_object.unique_id.endswith("motion_sensitivity"), + qualifier_func=lambda e: e.state.unique_id.endswith("motion_sensitivity"), ) # test that the state is in default medium state - assert entity.state["state"] == AqaraMotionSensitivities.Medium.name + assert entity.state.current_option == AqaraMotionSensitivities.Medium.name # send attribute report from device await send_attributes_report( zha_gateway, cluster, {"motion_sensitivity": AqaraMotionSensitivities.Low} ) - assert entity.state["state"] == AqaraMotionSensitivities.Low.name + assert entity.state.current_option == AqaraMotionSensitivities.Low.name assert entity._attr_entity_category == EntityCategory.CONFIG assert entity._attr_entity_registry_enabled_default is True @@ -242,7 +244,7 @@ async def test_on_off_select_attribute_report_v2( await entity.async_select_option(AqaraMotionSensitivities.Medium.name) await zha_gateway.async_block_till_done() - assert entity.state["state"] == AqaraMotionSensitivities.Medium.name + assert entity.state.current_option == AqaraMotionSensitivities.Medium.name assert cluster.write_attributes.call_count == 1 assert cluster.write_attributes.call_args == call( {"motion_sensitivity": AqaraMotionSensitivities.Medium}, @@ -268,17 +270,19 @@ async def test_non_zcl_select_state_restoration(zha_gateway: Gateway) -> None: entity = get_entity(zha_device, platform=Platform.SELECT, qualifier="WarningMode") - assert entity.state["state"] is None + assert entity.state.current_option is None entity.restore_external_state_attributes( state=security.IasWd.Warning.WarningMode.Burglar.name ) - assert entity.state["state"] == security.IasWd.Warning.WarningMode.Burglar.name + assert ( + entity.state.current_option == security.IasWd.Warning.WarningMode.Burglar.name + ) entity.restore_external_state_attributes( state=security.IasWd.Warning.WarningMode.Fire.name ) - assert entity.state["state"] == security.IasWd.Warning.WarningMode.Fire.name + assert entity.state.current_option == security.IasWd.Warning.WarningMode.Fire.name async def test_bega_color_temperature_channel_select(zha_gateway: Gateway) -> None: @@ -296,8 +300,8 @@ async def test_bega_color_temperature_channel_select(zha_gateway: Gateway) -> No platform=Platform.SELECT, qualifier="switchable_white", ) - assert entity.state["state"] == "Warm white" - assert entity.info_object.options == ["Warm white", "Cool white"] + assert entity.state.current_option == "Warm white" + assert entity.state.options == ["Warm white", "Cool white"] # send attribute report from device await send_attributes_report( @@ -305,7 +309,7 @@ async def test_bega_color_temperature_channel_select(zha_gateway: Gateway) -> No cluster, {"switchable_white": BegaColorTemperatureChannel.Cool_white}, ) - assert entity.state["state"] == "Cool white" + assert entity.state.current_option == "Cool white" # test selecting an option Write_Attributes_rsp = foundation.GENERAL_COMMANDS[ @@ -327,7 +331,7 @@ async def test_bega_color_temperature_channel_select(zha_gateway: Gateway) -> No ): await entity.async_select_option("Warm white") await zha_gateway.async_block_till_done() - assert entity.state["state"] == "Warm white" + assert entity.state.current_option == "Warm white" assert cluster.write_attributes.call_count == 1 assert cluster.write_attributes.call_args == call( {"switchable_white": BegaColorTemperatureChannel.Warm_white}, diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 1759f2edd..0ae27554c 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -157,7 +157,7 @@ async def async_test_temperature( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test temperature sensor.""" - assert entity.extra_state_attribute_names is None + assert entity.state.extra_state_attribute_names == frozenset() await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 2900, 2: 100}) assert_state(entity, 29.0, "°C") @@ -188,7 +188,7 @@ async def async_test_metering( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test Smart Energy metering sensor.""" - assert entity.extra_state_attribute_names == { + assert entity.state.extra_state_attribute_names == { "status", "device_type", "zcl_unit_of_measurement", @@ -197,12 +197,12 @@ async def async_test_metering( zha_gateway, cluster, {1025: 1, 1024: 12345, 1026: 100} ) assert_state(entity, 12345.0, None) - assert entity.state["status"] == "NO_ALARMS" - assert entity.state["device_type"] == "Electric Metering" + assert entity.state.status == "NO_ALARMS" + assert entity.state.device_type == "Electric Metering" await send_attributes_report(zha_gateway, cluster, {1024: 12346, "status": 64 + 8}) assert_state(entity, 12346.0, None) - assert entity.state["status"] in ( + assert entity.state.status in ( "SERVICE_DISCONNECT|POWER_FAILURE", "POWER_FAILURE|SERVICE_DISCONNECT", ) @@ -210,7 +210,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 1} ) - assert entity.state["status"] in ( + assert entity.state.status in ( "SERVICE_DISCONNECT|NOT_DEFINED", "NOT_DEFINED|SERVICE_DISCONNECT", ) @@ -218,7 +218,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 2} ) - assert entity.state["status"] in ( + assert entity.state.status in ( "SERVICE_DISCONNECT|PIPE_EMPTY", "PIPE_EMPTY|SERVICE_DISCONNECT", ) @@ -226,7 +226,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 5} ) - assert entity.state["status"] in ( + assert entity.state.status in ( "SERVICE_DISCONNECT|TEMPERATURE_SENSOR", "TEMPERATURE_SENSOR|SERVICE_DISCONNECT", ) @@ -235,14 +235,14 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 32, "metering_device_type": 4} ) - assert entity.state["status"] in ("", "32") + assert entity.state.status in ("", "32") async def async_test_smart_energy_summation_delivered( zha_gateway: Gateway, cluster, entity ): """Test SmartEnergy Summation delivered sensor.""" - assert entity.extra_state_attribute_names == { + assert entity.state.extra_state_attribute_names == { "status", "device_type", "zcl_unit_of_measurement", @@ -251,9 +251,9 @@ async def async_test_smart_energy_summation_delivered( zha_gateway, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100} ) assert_state(entity, 12.321, UnitOfEnergy.KILO_WATT_HOUR) - assert entity.state["status"] == "NO_ALARMS" - assert entity.state["device_type"] == "Electric Metering" - assert entity.info_object.device_class == SensorDeviceClass.ENERGY + assert entity.state.status == "NO_ALARMS" + assert entity.state.device_type == "Electric Metering" + assert entity.state.device_class == SensorDeviceClass.ENERGY async def async_test_smart_energy_summation_received( @@ -265,9 +265,9 @@ async def async_test_smart_energy_summation_received( zha_gateway, cluster, {1025: 1, "current_summ_received": 12321, 1026: 100} ) assert_state(entity, 12.321, UnitOfEnergy.KILO_WATT_HOUR) - assert entity.state["status"] == "NO_ALARMS" - assert entity.state["device_type"] == "Electric Metering" - assert entity.info_object.device_class == SensorDeviceClass.ENERGY + assert entity.state.status == "NO_ALARMS" + assert entity.state.device_type == "Electric Metering" + assert entity.state.device_class == SensorDeviceClass.ENERGY async def async_test_smart_energy_summation( @@ -279,8 +279,8 @@ async def async_test_smart_energy_summation( zha_gateway, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100} ) assert_state(entity, 12.32, "m³") - assert entity.state["status"] == "NO_ALARMS" - assert entity.state["device_type"] == "Electric Metering" + assert entity.state.status == "NO_ALARMS" + assert entity.state.device_type == "Electric Metering" async def async_test_electrical_measurement( @@ -311,7 +311,7 @@ async def async_test_electrical_measurement( assert_state(entity, 9.9, "W") await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050D: 88}) - assert entity.state["active_power_max"] == 8.8 + assert entity.state.max_value == 8.8 async def async_test_em_apparent_power( @@ -338,7 +338,7 @@ async def async_test_em_power_factor( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ): """Test electrical measurement Power Factor sensor.""" - assert entity.extra_state_attribute_names == {"measurement_type"} + assert entity.state.extra_state_attribute_names == {"measurement_type"} # update divisor cached value await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 1}) @@ -377,14 +377,17 @@ async def async_test_em_rms_current( assert_state(entity, 123.6, "A") await send_attributes_report(zha_gateway, cluster, {0: 1, current_max_attrid: 88}) - assert entity.state[current_max_attr_name] == 8.8 + assert entity.state.max_value == 8.8 async def async_test_em_rms_voltage( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test electrical measurement RMS Voltage sensor.""" - assert entity.extra_state_attribute_names == {"measurement_type", "rms_voltage_max"} + assert entity.state.extra_state_attribute_names == { + "measurement_type", + "rms_voltage_max", + } await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0505: 1234}) assert_state(entity, 123.4, "V") @@ -397,25 +400,25 @@ async def async_test_em_rms_voltage( assert_state(entity, 22.36, "V") await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0507: 888}) - assert entity.state["rms_voltage_max"] == 8.88 + assert entity.state.max_value == 8.88 async def async_test_powerconfiguration( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test powerconfiguration/battery sensor.""" - assert entity.extra_state_attribute_names == { + assert entity.state.extra_state_attribute_names == { "battery_voltage", "battery_quantity", "battery_size", } await send_attributes_report(zha_gateway, cluster, {33: 98}) assert_state(entity, 49, "%") - assert entity.state["battery_voltage"] == 2.9 - assert entity.state["battery_quantity"] == 3 - assert entity.state["battery_size"] == "AAA" + assert entity.state.battery_voltage == 2.9 + assert entity.state.battery_quantity == 3 + assert entity.state.battery_size == "AAA" await send_attributes_report(zha_gateway, cluster, {32: 20}) - assert entity.state["battery_voltage"] == 2.0 + assert entity.state.battery_voltage == 2.0 async def async_test_powerconfiguration2( @@ -446,7 +449,7 @@ async def async_test_setpoint_change_source( cluster, {hvac.Thermostat.AttributeDefs.setpoint_change_source.id: 0x01}, ) - assert entity.state["state"] == "Schedule" + assert entity.state.native_value == "Schedule" async def async_test_pi_heating_demand( @@ -468,14 +471,14 @@ async def async_test_change_source_timestamp( cluster, {hvac.Thermostat.AttributeDefs.setpoint_change_source_timestamp.id: 781355715}, ) - assert entity.state["state"] == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC) + assert entity.state.native_value == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC) async def async_test_em_dc_voltage( zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity ) -> None: """Test electrical measurement DC Voltage sensor.""" - assert entity.extra_state_attribute_names == {"measurement_type"} + assert entity.state.extra_state_attribute_names == {"measurement_type"} await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0100: 1234}) assert_state(entity, 123.4, "V") @@ -799,13 +802,13 @@ async def test_analog_input_simple(zha_gateway: Gateway) -> None: zha_dev, platform=Platform.SENSOR, exact_entity_type=AnalogInputSensor ) - assert entity.state["available"] is True - assert entity.state["state"] == 2.1322579383850098 - assert entity.info_object.fallback_name == "Some description" - assert entity.info_object.translation_key is None - assert entity.info_object.unit == UnitOfElectricPotential.VOLT - assert entity.info_object.device_class is None - assert entity.info_object.suggested_display_precision is None + assert entity.state.available is True + assert entity.state.native_value == 2.1322579383850098 + assert entity.state.fallback_name == "Some description" + assert entity.state.translation_key is None + assert entity.state.unit == UnitOfElectricPotential.VOLT + assert entity.state.device_class is None + assert entity.state.suggested_display_precision is None async def test_analog_input_ignored(zha_gateway: Gateway) -> None: @@ -874,13 +877,13 @@ async def test_analog_input_complex(zha_gateway: Gateway) -> None: zha_dev, platform=Platform.SENSOR, exact_entity_type=AnalogInputSensor ) - assert entity.state["available"] is True - assert entity.state["state"] == 2.1322579383850098 - assert entity.info_object.fallback_name == "Some description" - assert entity.info_object.translation_key is None - assert entity.info_object.unit is PERCENTAGE # overridden! - assert entity.info_object.device_class is SensorDeviceClass.HUMIDITY # overridden! - assert entity.info_object.suggested_display_precision == 2 + assert entity.state.available is True + assert entity.state.native_value == 2.1322579383850098 + assert entity.state.fallback_name == "Some description" + assert entity.state.translation_key is None + assert entity.state.unit is PERCENTAGE # overridden! + assert entity.state.device_class is SensorDeviceClass.HUMIDITY # overridden! + assert entity.state.suggested_display_precision == 2 def assert_state(entity: PlatformEntity, state: Any, unit_of_measurement: str) -> None: @@ -889,8 +892,8 @@ def assert_state(entity: PlatformEntity, state: Any, unit_of_measurement: str) - This is used to ensure that the logic in each sensor class handled the attribute report it received correctly. """ - assert entity.state["state"] == state - assert entity.info_object.unit == unit_of_measurement + assert entity.state.native_value == state + assert entity.state.unit == unit_of_measurement async def test_electrical_measurement_init(zha_gateway: Gateway) -> None: @@ -923,21 +926,21 @@ async def test_electrical_measurement_init(zha_gateway: Gateway) -> None: cluster, {EMAttrs.active_power.id: 100}, ) - assert entity.state["state"] == 100 + assert entity.state.native_value == 100 await send_attributes_report( zha_gateway, cluster, {EMAttrs.active_power.id: 30, EMAttrs.ac_power_divisor.id: 10}, ) - assert entity.state["state"] == 3.0 + assert entity.state.native_value == 3.0 await send_attributes_report( zha_gateway, cluster, {EMAttrs.active_power.id: 30, EMAttrs.ac_power_multiplier.id: 20}, ) - assert entity.state["state"] == 60.0 + assert entity.state.native_value == 60.0 @pytest.mark.parametrize( @@ -1240,7 +1243,7 @@ async def test_elec_measurement_sensor_type( platform=Platform.SENSOR, entity_type=sensor.ElectricalMeasurementApparentPower, ) - assert entity.state["measurement_type"] == expected_type + assert entity.state.measurement_type == expected_type async def test_elec_measurement_sensor_polling(zha_gateway: Gateway) -> None: @@ -1259,7 +1262,7 @@ async def test_elec_measurement_sensor_polling(zha_gateway: Gateway) -> None: platform=Platform.SENSOR, exact_entity_type=sensor.ElectricalMeasurementActivePower, ) - assert entity.state["state"] == 2.0 + assert entity.state.native_value == 2.0 # update the value for the power reading zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = ( @@ -1267,14 +1270,14 @@ async def test_elec_measurement_sensor_polling(zha_gateway: Gateway) -> None: ) # ensure the state is still 2.0 - assert entity.state["state"] == 2.0 + assert entity.state.native_value == 2.0 # let the polling happen await asyncio.sleep(90) await zha_gateway.async_block_till_done(wait_background_tasks=True) # ensure the state has been updated to 6.0 - assert entity.state["state"] == 6.0 + assert entity.state.native_value == 6.0 async def test_metering_sensor_polling(zha_gateway: Gateway) -> None: @@ -1293,7 +1296,7 @@ async def test_metering_sensor_polling(zha_gateway: Gateway) -> None: platform=Platform.SENSOR, exact_entity_type=sensor.SmartEnergySummation, ) - assert entity.state["state"] == 2.0 + assert entity.state.native_value == 2.0 # update the value for the power reading zigpy_dev.endpoints[1].smartenergy_metering.PLUGGED_ATTR_READS[ @@ -1301,14 +1304,14 @@ async def test_metering_sensor_polling(zha_gateway: Gateway) -> None: ] = 6000 # ensure the state is still 2.0 - assert entity.state["state"] == 2.0 + assert entity.state.native_value == 2.0 # let the polling happen await asyncio.sleep(90) await zha_gateway.async_block_till_done(wait_background_tasks=True) # ensure the state has been updated to 6.0 - assert entity.state["state"] == 6.0 + assert entity.state.native_value == 6.0 @pytest.mark.parametrize( @@ -1464,7 +1467,7 @@ async def test_timestamp_sensor_v2(zha_gateway: Gateway) -> None: entity = get_entity(zha_device, platform=Platform.SENSOR, qualifier="start_time") await send_attributes_report(zha_gateway, cluster, {"start_time": 781355715}) - assert entity.state["state"] == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC) + assert entity.state.native_value == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC) class OppleCluster(CustomCluster, ManufacturerSpecificCluster): @@ -1584,12 +1587,12 @@ async def test_state_class( power_entity = get_entity( zha_device, platform=Platform.SENSOR, - qualifier_func=lambda e: e.info_object.unique_id.endswith("power"), + qualifier_func=lambda e: e.state.unique_id.endswith("power"), ) energy_entity = get_entity( zha_device, platform=Platform.SENSOR, - qualifier_func=lambda e: e.info_object.unique_id.endswith("energy"), + qualifier_func=lambda e: e.state.unique_id.endswith("energy"), ) energy_delivered_entity = get_entity( zha_device, platform=Platform.SENSOR, qualifier="energy_delivered" @@ -1713,12 +1716,10 @@ async def test_device_counter_sensors(zha_gateway: Gateway) -> None: entity = get_entity( coordinator, platform=Platform.SENSOR, - qualifier_func=lambda e: e.info_object.unique_id.endswith( - "ezsp_counters_counter_1" - ), + qualifier_func=lambda e: e.state.unique_id.endswith("ezsp_counters_counter_1"), ) - assert entity.state["state"] == 1 + assert entity.state.native_value == 1 # simulate counter increment on application coordinator.device.application.state.counters["ezsp_counters"][ @@ -1728,7 +1729,7 @@ async def test_device_counter_sensors(zha_gateway: Gateway) -> None: await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["state"] == 2 + assert entity.state.native_value == 2 # test disabling the entity disables it and removes it from the updater assert len(zha_gateway.global_updater._update_listeners) == 3 @@ -1769,14 +1770,14 @@ async def test_device_unavailable_or_disabled_skips_entity_polling( exact_entity_type=sensor.RSSISensor, ) - assert entity.state["state"] is None + assert entity.state.native_value is None elec_measurement_zha_dev.device.rssi = 60 await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2) await zha_gateway.async_block_till_done(wait_background_tasks=True) - assert entity.state["state"] == 60 + assert entity.state.native_value == 60 assert entity.enabled is True assert len(zha_gateway.global_updater._update_listeners) == 5 @@ -1877,10 +1878,10 @@ async def test_danfoss_thermostat_sw_error(zha_gateway: Gateway) -> None: }, ) - assert entity.state["state"] == "something" - assert entity.extra_state_attribute_names - assert "Top_pcb_sensor_error" in entity.extra_state_attribute_names - assert entity.state["Top_pcb_sensor_error"] + assert entity.state.native_value == "something" + assert entity.state.extra_state_attribute_names + assert "Top_pcb_sensor_error" in entity.state.extra_state_attribute_names + assert entity.state.bit_states["Top_pcb_sensor_error"] async def test_quirks_sensor_attr_converter(zha_gateway: Gateway) -> None: @@ -1926,10 +1927,10 @@ async def test_quirks_sensor_attr_converter(zha_gateway: Gateway) -> None: # send updated value, check if the value is converted await send_attributes_report(zha_gateway, cluster, {"present_value": 100}) - assert entity.state["state"] == 200.0 + assert entity.state.native_value == 200.0 await send_attributes_report(zha_gateway, cluster, {"present_value": 0}) - assert entity.state["state"] == 100.0 + assert entity.state.native_value == 100.0 async def test_ignore_non_value(zha_gateway: Gateway) -> None: @@ -1944,7 +1945,7 @@ async def test_ignore_non_value(zha_gateway: Gateway) -> None: cluster = zha_device.device.endpoints[1].temperature entity = get_entity(zha_device, platform=Platform.SENSOR, entity_type=Temperature) - assert entity.state["state"] == 22.3 + assert entity.state.native_value == 22.3 # Normal attribute report await send_attributes_report( @@ -1952,7 +1953,7 @@ async def test_ignore_non_value(zha_gateway: Gateway) -> None: cluster, {measurement.TemperatureMeasurement.AttributeDefs.measured_value.id: 3000}, ) - assert entity.state["state"] == 30.0 + assert entity.state.native_value == 30.0 # Invalid attribute value, ignored await send_attributes_report( @@ -1960,7 +1961,7 @@ async def test_ignore_non_value(zha_gateway: Gateway) -> None: cluster, {measurement.TemperatureMeasurement.AttributeDefs.measured_value.id: -0x8000}, ) - assert entity.state["state"] is None + assert entity.state.native_value is None async def test_ignore_non_value_quirks_v2(zha_gateway: Gateway) -> None: @@ -1978,11 +1979,11 @@ async def test_ignore_non_value_quirks_v2(zha_gateway: Gateway) -> None: # Normal attribute report await send_attributes_report(zha_gateway, cluster, {"measured_value": 100}) - assert entity.state["state"] == 100 + assert entity.state.native_value == 100 # Invalid attribute value (uint16 non_value), should be ignored await send_attributes_report(zha_gateway, cluster, {"measured_value": 0xFFFF}) - assert entity.state["state"] is None + assert entity.state.native_value is None async def test_ignore_nan_value(zha_gateway: Gateway) -> None: @@ -2002,7 +2003,7 @@ async def test_ignore_nan_value(zha_gateway: Gateway) -> None: ) # Initial value from the diagnostics file (0.0 * 1e6 = 0.0 ppm) - assert entity.state["state"] == 0.0 + assert entity.state.native_value == 0.0 # Normal attribute report await send_attributes_report( @@ -2012,7 +2013,7 @@ async def test_ignore_nan_value(zha_gateway: Gateway) -> None: measurement.CarbonMonoxideConcentration.AttributeDefs.measured_value.id: 0.001 }, ) - assert entity.state["state"] == 1000.0 + assert entity.state.native_value == 1000.0 # NaN attribute value should result in None state await send_attributes_report( @@ -2024,7 +2025,7 @@ async def test_ignore_nan_value(zha_gateway: Gateway) -> None: ), }, ) - assert entity.state["state"] is None + assert entity.state.native_value is None @pytest.mark.parametrize( @@ -2102,14 +2103,14 @@ async def test_enum_sensor(zha_gateway: Gateway) -> None: zha_device = await join_zigpy_device(zha_gateway, zigpy_dev) entity = get_entity(zha_device, platform=Platform.SENSOR, qualifier="battery_size") - assert entity.state["state"] == "AAA" + assert entity.state.native_value == "AAA" zigpy_dev.endpoints[1].power.update_attribute( PowerConfiguration.AttributeDefs.battery_size.id, 0xAB, # unknown ) - assert entity.state["state"] == "undefined_0xab" # TODO: should this be `None`? + assert entity.state.native_value == "undefined_0xab" # TODO: should this be `None`? async def test_em_poller_runs_independently_of_entity_enabled_state( diff --git a/tests/test_siren.py b/tests/test_siren.py index 997f28d41..9d9338625 100644 --- a/tests/test_siren.py +++ b/tests/test_siren.py @@ -64,7 +64,7 @@ async def test_siren(zha_gateway: Gateway) -> None: | SirenEntityFeature.DURATION ) - assert entity.state["state"] is False + assert entity.state.is_on is False # turn on from client with patch( @@ -84,7 +84,7 @@ async def test_siren(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to on - assert entity.state["state"] is True + assert entity.state.is_on is True # turn off from client with patch( @@ -104,7 +104,7 @@ async def test_siren(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to off - assert entity.state["state"] is False + assert entity.state.is_on is False # turn on from client with options with patch( @@ -124,7 +124,7 @@ async def test_siren(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to on - assert entity.state["state"] is True + assert entity.state.is_on is True async def test_basic_siren(zha_gateway: Gateway) -> None: @@ -140,7 +140,7 @@ async def test_basic_siren(zha_gateway: Gateway) -> None: | SirenEntityFeature.DURATION ) - assert entity.state["state"] is False + assert entity.state.is_on is False # turn on from client with patch( @@ -160,7 +160,7 @@ async def test_basic_siren(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to on - assert entity.state["state"] is True + assert entity.state.is_on is True # turn off from client with patch( @@ -180,7 +180,7 @@ async def test_basic_siren(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to off - assert entity.state["state"] is False + assert entity.state.is_on is False # turn on from client with duration option with patch( @@ -200,7 +200,7 @@ async def test_basic_siren(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to on - assert entity.state["state"] is True + assert entity.state.is_on is True async def test_siren_timed_off(zha_gateway: Gateway) -> None: @@ -210,7 +210,7 @@ async def test_siren_timed_off(zha_gateway: Gateway) -> None: entity = get_entity(zha_device, platform=Platform.SIREN) - assert entity.state["state"] is False + assert entity.state.is_on is False # turn on from client with patch( @@ -230,9 +230,9 @@ async def test_siren_timed_off(zha_gateway: Gateway) -> None: cluster.request.reset_mock() # test that the state has changed to on - assert entity.state["state"] is True + assert entity.state.is_on is True await asyncio.sleep(6) # test that the state has changed to off from the timer - assert entity.state["state"] is False + assert entity.state.is_on is False diff --git a/tests/test_switch.py b/tests/test_switch.py index a468c2015..3213d1970 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -124,15 +124,15 @@ async def test_switch(zha_gateway: Gateway) -> None: cluster = zigpy_device.endpoints.get(1).on_off entity: PlatformEntity = get_entity(zha_device, Platform.SWITCH) - assert bool(bool(entity.state["state"])) is False + assert bool(bool(entity.state.is_on)) is False # turn on at switch await send_attributes_report(zha_gateway, cluster, {1: 0, 0: 1, 2: 2}) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn off at switch await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 0, 2: 2}) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn on from client with patch( @@ -141,7 +141,7 @@ async def test_switch(zha_gateway: Gateway) -> None: ): await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -161,7 +161,7 @@ async def test_switch(zha_gateway: Gateway) -> None: ): await entity.async_turn_off() await zha_gateway.async_block_till_done() - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -178,7 +178,7 @@ async def test_switch(zha_gateway: Gateway) -> None: ): await entity.async_turn_off() await zha_gateway.async_block_till_done() - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -198,7 +198,7 @@ async def test_switch(zha_gateway: Gateway) -> None: ): await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -210,7 +210,7 @@ async def test_switch(zha_gateway: Gateway) -> None: # test updating entity state from client cluster.read_attributes.reset_mock() - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False cluster.PLUGGED_ATTR_READS = {"on_off": True} await entity.async_update() await zha_gateway.async_block_till_done() @@ -218,7 +218,7 @@ async def test_switch(zha_gateway: Gateway) -> None: assert cluster.read_attributes.await_args == call( ["on_off"], allow_cache=False, only_cache=False, manufacturer=UNDEFINED ) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True async def test_zha_group_switch_entity(zha_gateway: Gateway) -> None: @@ -244,14 +244,14 @@ async def test_zha_group_switch_entity(zha_gateway: Gateway) -> None: entity: GroupEntity = get_group_entity(zha_group, platform=Platform.SWITCH) assert entity.group_id == zha_group.group_id - assert entity.info_object.fallback_name == zha_group.name + assert entity.state.fallback_name == zha_group.name group_cluster_on_off = zha_group.zigpy_group.endpoint[general.OnOff.cluster_id] dev1_cluster_on_off = device_switch_1.device.endpoints[1].on_off dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off # test that the lights were created and are off - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn on from HA with patch( @@ -269,7 +269,7 @@ async def test_zha_group_switch_entity(zha_gateway: Gateway) -> None: expect_reply=True, manufacturer=None, ) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn off from HA with patch( @@ -287,7 +287,7 @@ async def test_zha_group_switch_entity(zha_gateway: Gateway) -> None: expect_reply=True, manufacturer=None, ) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # test some of the group logic to make sure we key off states correctly await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 1}) @@ -295,40 +295,40 @@ async def test_zha_group_switch_entity(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() # group member updates are debounced - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False await asyncio.sleep(1) await zha_gateway.async_block_till_done() # test that group light is on - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() # test that group light is still on - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True await send_attributes_report(zha_gateway, dev2_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() # group member updates are debounced - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True await asyncio.sleep(1) await zha_gateway.async_block_till_done() # test that group light is now off - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 1}) await zha_gateway.async_block_till_done() # group member updates are debounced - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False await asyncio.sleep(1) await zha_gateway.async_block_till_done() # test that group light is now back on - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True await group_entity_availability_test( zha_gateway, device_switch_1, device_switch_2, entity @@ -392,19 +392,19 @@ async def test_switch_configurable( entity = get_entity(zha_device, platform=Platform.SWITCH) # test that the state has changed from unavailable to off - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn on at switch await send_attributes_report( zha_gateway, cluster, {"window_detection_function": True} ) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn off at switch await send_attributes_report( zha_gateway, cluster, {"window_detection_function": False} ) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn on from HA with patch( @@ -523,15 +523,15 @@ async def test_switch_configurable_custom_on_off_values(zha_gateway: Gateway) -> entity = get_entity(zha_device, platform=Platform.SWITCH) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn on at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 3}) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn off at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 5}) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn on from HA with patch( @@ -605,15 +605,15 @@ async def test_switch_configurable_custom_on_off_values_force_inverted( entity = get_entity(zha_device, platform=Platform.SWITCH) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn on at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 3}) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn off at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 5}) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn on from HA with patch( @@ -690,15 +690,15 @@ async def test_switch_configurable_custom_on_off_values_inverter_attribute( entity = get_entity(zha_device, platform=Platform.SWITCH) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn on at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 3}) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # turn off at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 5}) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True # turn on from HA with patch( @@ -764,13 +764,13 @@ async def test_cover_inversion_switch(zha_gateway: Gateway) -> None: await entity.async_update() await zha_gateway.async_block_till_done() assert cluster.read_attributes.call_count == prev_call_count + 1 - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False # test to see the state remains after tilting to 0% await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 0} ) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False with patch( "zigpy.zcl.Cluster.write_attributes", return_value=[0x1, zcl_f.Status.SUCCESS] @@ -791,7 +791,7 @@ async def test_cover_inversion_switch(zha_gateway: Gateway) -> None: manufacturer=UNDEFINED, ) - assert bool(entity.state["state"]) is True + assert bool(entity.state.is_on) is True cluster.write_attributes.reset_mock() @@ -807,7 +807,7 @@ async def test_cover_inversion_switch(zha_gateway: Gateway) -> None: manufacturer=UNDEFINED, ) - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False cluster.write_attributes.reset_mock() @@ -816,7 +816,7 @@ async def test_cover_inversion_switch(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() assert cluster.write_attributes.call_count == 0 - assert bool(entity.state["state"]) is False + assert bool(entity.state.is_on) is False async def test_cover_inversion_switch_not_created(zha_gateway: Gateway) -> None: @@ -862,8 +862,8 @@ async def test_binary_output_cluster(zha_gateway: Gateway) -> None: # Clear out the attribute first, to test handling of the missing state cluster.update_attribute(BinaryOutput.AttributeDefs.present_value.id, None) - assert switch_entity.info_object.fallback_name == "Entity Description" - assert switch_entity.state["state"] is False + assert switch_entity.state.fallback_name == "Entity Description" + assert switch_entity.state.is_on is False # Turn it on cluster.write_attributes.reset_mock() @@ -871,7 +871,7 @@ async def test_binary_output_cluster(zha_gateway: Gateway) -> None: assert cluster.write_attributes.mock_calls == [ call({"present_value": True}, manufacturer=UNDEFINED) ] - assert switch_entity.state["state"] is True + assert switch_entity.state.is_on is True # Turn it off cluster.write_attributes.reset_mock() @@ -879,7 +879,7 @@ async def test_binary_output_cluster(zha_gateway: Gateway) -> None: assert cluster.write_attributes.mock_calls == [ call({"present_value": False}, manufacturer=UNDEFINED) ] - assert switch_entity.state["state"] is False + assert switch_entity.state.is_on is False # Report an attribute change await send_attributes_report( @@ -887,14 +887,14 @@ async def test_binary_output_cluster(zha_gateway: Gateway) -> None: cluster, {BinaryOutput.AttributeDefs.present_value.id: t.Bool(False)}, ) - assert switch_entity.state["state"] is False + assert switch_entity.state.is_on is False # Force an update cluster.read_attributes.reset_mock() cluster.PLUGGED_ATTR_READS = {BinaryOutput.AttributeDefs.present_value.name: True} await switch_entity.async_update() - assert switch_entity.state["state"] is True + assert switch_entity.state.is_on is True assert cluster.read_attributes.mock_calls == [ call( diff --git a/tests/test_update.py b/tests/test_update.py index 987bd535c..7cc06625b 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -26,15 +26,6 @@ ) from zha.application import Platform from zha.application.gateway import Gateway -from zha.application.platforms.update import ( - ATTR_IN_PROGRESS, - ATTR_INSTALLED_VERSION, - ATTR_LATEST_VERSION, - ATTR_RELEASE_NOTES, - ATTR_RELEASE_SUMMARY, - ATTR_RELEASE_URL, - ATTR_UPDATE_PERCENTAGE, -) from zha.exceptions import ZHAException @@ -178,8 +169,8 @@ async def test_firmware_update_notification_from_zigpy(zha_gateway: Gateway) -> ) entity = get_entity(zha_device, platform=Platform.UPDATE) - assert entity.state["installed_version"] == f"0x{installed_fw_version:08x}" - assert entity.state["latest_version"] is None + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert entity.state.latest_version is None # simulate an image available notification await ota_cluster._handle_query_next_image( @@ -196,11 +187,10 @@ async def test_firmware_update_notification_from_zigpy(zha_gateway: Gateway) -> ) await zha_gateway.async_block_till_done() - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert entity.state[ATTR_IN_PROGRESS] is False + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert entity.state.in_progress is False assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -255,11 +245,10 @@ async def fake_reinterview(): ) await zha_gateway.async_block_till_done() - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert entity.state[ATTR_IN_PROGRESS] is False + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert entity.state.in_progress is False assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) ota_completed = False @@ -353,15 +342,15 @@ async def endpoint_reply(cluster, sequence, data, **kwargs): # make sure the state machine gets progress reports assert ( - entity.state[ATTR_INSTALLED_VERSION] + entity.state.installed_version == f"0x{installed_fw_version:08x}" ) - assert entity.state[ATTR_IN_PROGRESS] is True - assert entity.state[ATTR_UPDATE_PERCENTAGE] == pytest.approx( + assert entity.state.in_progress is True + assert entity.state.update_percentage == pytest.approx( 100 * (40 / 70) ) assert ( - entity.state[ATTR_LATEST_VERSION] + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -409,16 +398,16 @@ def read_new_fw_version(*args, **kwargs): await zha_gateway.async_block_till_done() assert ( - entity.state[ATTR_INSTALLED_VERSION] + entity.state.installed_version == f"0x{fw_image.firmware.header.file_version:08x}" ) - assert not entity.state[ATTR_IN_PROGRESS] - assert entity.state[ATTR_LATEST_VERSION] == entity.state[ATTR_INSTALLED_VERSION] + assert not entity.state.in_progress + assert entity.state.latest_version == entity.state.installed_version # If we send a progress notification incorrectly, it won't be handled entity._update_progress(50, 100, 0.50) - assert not entity.state[ATTR_IN_PROGRESS] + assert not entity.state.in_progress # Post-OTA reinterview should have swapped the zigpy device and rebuilt ZHA assert zha_device.device is not old_zigpy_device @@ -450,11 +439,10 @@ async def test_firmware_update_raises(zha_gateway: Gateway) -> None: ) await zha_gateway.async_block_till_done() - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert not entity.state[ATTR_IN_PROGRESS] + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert not entity.state.in_progress assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) async def endpoint_reply(cluster, sequence, data, **kwargs): @@ -546,7 +534,7 @@ async def test_firmware_update_empty_exception_message(zha_gateway: Gateway) -> assert str(exc_info.value) == "Update was not successful: TimeoutError()" assert exc_info.value.__cause__ is raised - assert not entity.state[ATTR_IN_PROGRESS] + assert not entity.state.in_progress async def test_firmware_update_downgrade(zha_gateway: Gateway) -> None: @@ -582,11 +570,10 @@ async def test_firmware_update_downgrade(zha_gateway: Gateway) -> None: ) await zha_gateway.async_block_till_done() - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert not entity.state[ATTR_IN_PROGRESS] + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert not entity.state.in_progress assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) with patch.object( @@ -614,13 +601,12 @@ async def test_firmware_update_downgrade(zha_gateway: Gateway) -> None: ] assert ( - entity.state[ATTR_INSTALLED_VERSION] + entity.state.installed_version == f"0x{fw_image_downgrade.firmware.header.file_version:08x}" ) - assert not entity.state[ATTR_IN_PROGRESS] + assert not entity.state.in_progress assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -655,16 +641,16 @@ async def test_firmware_update_no_image(zha_gateway: Gateway) -> None: ) await zha_gateway.async_block_till_done() - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert not entity.state[ATTR_IN_PROGRESS] - assert entity.state[ATTR_LATEST_VERSION] is None + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert not entity.state.in_progress + assert entity.state.latest_version is None with pytest.raises(ZHAException): await entity.async_install(version=None) - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert not entity.state[ATTR_IN_PROGRESS] - assert entity.state[ATTR_LATEST_VERSION] is None + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert not entity.state.in_progress + assert entity.state.latest_version is None async def test_firmware_update_latest_version_even_if_downgrade( @@ -704,13 +690,13 @@ async def test_firmware_update_latest_version_even_if_downgrade( ) await zha_gateway.async_block_till_done() - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert not entity.state[ATTR_IN_PROGRESS] + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert not entity.state.in_progress assert ( - entity.state[ATTR_LATEST_VERSION] + entity.state.latest_version == f"0x{fw_image_downgrade.firmware.header.file_version:08x}" ) - assert entity.state[ATTR_RELEASE_URL] == "https://example.com/releases/v0.1" + assert entity.state.release_url == "https://example.com/releases/v0.1" async def test_firmware_update_metadata(zha_gateway: Gateway) -> None: @@ -738,9 +724,9 @@ async def test_firmware_update_metadata(zha_gateway: Gateway) -> None: entity = get_entity(zha_device, platform=Platform.UPDATE) # metadata should be None before notification - assert entity.state[ATTR_RELEASE_SUMMARY] is None - assert entity.state[ATTR_RELEASE_NOTES] is None - assert entity.state[ATTR_RELEASE_URL] is None + assert entity.state.release_summary is None + assert entity.state.release_notes is None + assert entity.state.release_url is None # simulate an image available notification await ota_cluster._handle_query_next_image( @@ -759,16 +745,15 @@ async def test_firmware_update_metadata(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() # verify metadata is exposed in entity state now - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) - assert entity.state[ATTR_RELEASE_URL] == "https://example.com/releases/v1.0" - assert entity.state[ATTR_RELEASE_SUMMARY] == "This is a test changelog!" + assert entity.state.release_url == "https://example.com/releases/v1.0" + assert entity.state.release_summary == "This is a test changelog!" # release notes include version header - assert entity.state[ATTR_RELEASE_NOTES] == ( + assert entity.state.release_notes == ( f"## 0x{fw_image.firmware.header.file_version:08x}\n" "These are the full release notes." ) @@ -827,12 +812,12 @@ async def test_firmware_update_multiple_upgrades_combined_release_notes( # Verify latest version is the newest firmware assert ( - entity.state[ATTR_LATEST_VERSION] + entity.state.latest_version == f"0x{fw_image_v3.firmware.header.file_version:08x}" ) # Only latest firmware provides URL and release summary - assert entity.state[ATTR_RELEASE_URL] == "https://example.com/releases/v3" - assert entity.state[ATTR_RELEASE_SUMMARY] == "Latest changelog" + assert entity.state.release_url == "https://example.com/releases/v3" + assert entity.state.release_summary == "Latest changelog" # Release notes should be combined with version headers # fw_image_v2 is skipped because it has no release notes @@ -842,7 +827,7 @@ async def test_firmware_update_multiple_upgrades_combined_release_notes( f"## 0x{fw_image_v1.firmware.header.file_version:08x}\n" "Release notes for v1." ) - assert entity.state[ATTR_RELEASE_NOTES] == expected_release_notes + assert entity.state.release_notes == expected_release_notes async def test_firmware_update_cached_on_startup(zha_gateway: Gateway) -> None: @@ -879,10 +864,9 @@ async def test_firmware_update_cached_on_startup(zha_gateway: Gateway) -> None: await zha_gateway.async_block_till_done() entity = get_entity(zha_device, platform=Platform.UPDATE) - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" assert ( - entity.state[ATTR_LATEST_VERSION] - == f"0x{fw_image.firmware.header.file_version:08x}" + entity.state.latest_version == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -910,7 +894,7 @@ async def test_firmware_update_no_cached_query_on_startup( await zha_gateway.async_block_till_done() entity = get_entity(zha_device, platform=Platform.UPDATE) - assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" - assert entity.state[ATTR_LATEST_VERSION] is None + assert entity.state.installed_version == f"0x{installed_fw_version:08x}" + assert entity.state.latest_version is None # get_ota_images should not have been called since there's no cached query zigpy_device.application.ota.get_ota_images.assert_not_called() diff --git a/zha/application/gateway.py b/zha/application/gateway.py index ecff916d1..d53a1f520 100644 --- a/zha/application/gateway.py +++ b/zha/application/gateway.py @@ -661,7 +661,7 @@ def _emit_group_gateway_message( # pylint: disable=unused-argument gateway_message_type, GroupEvent( event=gateway_message_type, - group_info=zha_group.info_object, + group_info=zha_group.state, ), ) diff --git a/zha/application/platforms/__init__.py b/zha/application/platforms/__init__.py index 675918e88..12ba11934 100644 --- a/zha/application/platforms/__init__.py +++ b/zha/application/platforms/__init__.py @@ -172,8 +172,8 @@ class EntityCategory(StrEnum): @dataclasses.dataclass(frozen=True, kw_only=True) -class BaseEntityInfo: - """Information about a base entity.""" +class BaseEntityState: + """State for the base entity.""" fallback_name: str unique_id: str @@ -189,6 +189,8 @@ class BaseEntityInfo: enabled: bool = True primary: bool + extra_state_attribute_names: frozenset[str] + # For platform entities device_ieee: EUI64 | None endpoint_id: int | None @@ -232,6 +234,22 @@ class EntityStateChangedEvent: device_ieee: EUI64 | None = None endpoint_id: int | None = None group_id: int | None = None + state_diff: dict[str, Any] + + +def compute_state_diff( + old: BaseEntityState | None, new: BaseEntityState +) -> dict[str, Any]: + """Return the fields of `new` that differ from `old`.""" + new_values = new.__dict__ + + if old is None: + return dict(new_values) + + old_values = old.__dict__ + return { + name: value for name, value in new_values.items() if old_values[name] != value + } class BaseEntity(LogMixin, EventBase): @@ -254,6 +272,7 @@ async def async_initialize_cluster(self, cluster: Any) -> None: _attr_device_class: str | None = None _attr_state_class: str | None = None _attr_enabled: bool = True + _attr_extra_state_attribute_names: set[str] | None = None _attr_always_supported: bool = False _attr_primary: bool | None = None @@ -384,11 +403,10 @@ def identifiers(self) -> BaseIdentifiers: platform=self.PLATFORM, ) - @cached_property - def info_object(self) -> BaseEntityInfo: - """Return a representation of the platform entity.""" - - return BaseEntityInfo( + @property + def state(self) -> BaseEntityState: + """Return the state of this entity.""" + return BaseEntityState( unique_id=self.unique_id, migrate_unique_ids=self.migrate_unique_ids, platform=self.PLATFORM, @@ -402,6 +420,9 @@ def info_object(self) -> BaseEntityInfo: entity_registry_enabled_default=self.entity_registry_enabled_default, enabled=self.enabled, primary=self.primary, + extra_state_attribute_names=frozenset( + self._attr_extra_state_attribute_names or () + ), # Set by platform entities device_ieee=None, endpoint_id=None, @@ -410,24 +431,6 @@ def info_object(self) -> BaseEntityInfo: group_id=None, ) - @property - def state(self) -> dict[str, Any]: - """Return the arguments to use in the command.""" - return { - "class_name": self.__class__.__name__, - } - - @cached_property - def extra_state_attribute_names(self) -> set[str] | None: - """Return entity specific state attribute names. - - Implemented by platform classes. Convention for attribute names - is lowercase snake_case. - """ - if hasattr(self, "_attr_extra_state_attribute_names"): - return self._attr_extra_state_attribute_names - return None - def enable(self) -> None: """Enable the entity.""" self.enabled = True @@ -461,9 +464,14 @@ async def on_remove(self) -> None: def maybe_emit_state_changed_event(self) -> None: """Send the state of this platform entity.""" state = self.state - if self.__previous_state != state: + previous_state = self.__previous_state + if previous_state != state: self.emit( - STATE_CHANGED, EntityStateChangedEvent(**self.identifiers.__dict__) + STATE_CHANGED, + EntityStateChangedEvent( + **self.identifiers.__dict__, + state_diff=compute_state_diff(previous_state, state), + ), ) self.__previous_state = state @@ -488,6 +496,7 @@ class PlatformEntity(BaseEntity): # Per-cluster configuration (keyed by cluster ID) _server_cluster_config: Mapping[int, ClusterConfig] = MappingProxyType({}) + _client_cluster_config: Mapping[int, ClusterConfig] = MappingProxyType({}) def __init__( @@ -594,16 +603,6 @@ def identifiers(self) -> PlatformEntityIdentifiers: endpoint_id=self.endpoint.id, ) - @cached_property - def info_object(self) -> BaseEntityInfo: - """Return a representation of the platform entity.""" - return dataclasses.replace( - super().info_object, - device_ieee=self._device.ieee, - endpoint_id=self._endpoint.id, - available=self.available, - ) - @property def device(self) -> Device: """Return the device.""" @@ -664,13 +663,6 @@ def available(self) -> bool: """Return true if the device this entity belongs to is available.""" return self.device.available - @property - def state(self) -> dict[str, Any]: - """Return the arguments to use in the command.""" - state = super().state - state["available"] = self.available - return state - async def async_update(self) -> None: """Retrieve latest state. @@ -678,6 +670,16 @@ async def async_update(self) -> None: own attributes directly from the relevant cluster(s). """ + @property + def state(self) -> BaseEntityState: + """Return the state of this entity.""" + return dataclasses.replace( + super().state, + device_ieee=self._device.ieee, + endpoint_id=self._endpoint.id, + available=self.available, + ) + class GroupEntity(BaseEntity): """A base class for group entities.""" @@ -708,21 +710,15 @@ def identifiers(self) -> GroupEntityIdentifiers: group_id=self.group_id, ) - @cached_property - def info_object(self) -> BaseEntityInfo: - """Return a representation of the group.""" + @property + def state(self) -> BaseEntityState: + """Return the state of this entity.""" return dataclasses.replace( - super().info_object, + super().state, + available=self.available, group_id=self.group_id, ) - @property - def state(self) -> dict[str, Any]: - """Return the arguments to use in the command.""" - state = super().state - state["available"] = self.available - return state - @property def available(self) -> bool: """Return true if all member entities are available.""" diff --git a/zha/application/platforms/alarm_control_panel/__init__.py b/zha/application/platforms/alarm_control_panel/__init__.py index 2c6b82927..b1e5e5e4f 100644 --- a/zha/application/platforms/alarm_control_panel/__init__.py +++ b/zha/application/platforms/alarm_control_panel/__init__.py @@ -4,8 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable -from dataclasses import dataclass -import functools +import dataclasses import logging from typing import TYPE_CHECKING, Any @@ -19,7 +18,7 @@ from zha.application import Platform from zha.application.platforms import ( - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, PlatformEntity, @@ -46,14 +45,14 @@ SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered" -@dataclass(frozen=True, kw_only=True) -class AlarmControlPanelEntityInfo(BaseEntityInfo): - """Alarm control panel entity info.""" +@dataclasses.dataclass(frozen=True, kw_only=True) +class AlarmControlPanelState(BaseEntityState): + """State for alarm control panel entities.""" + alarm_state: AlarmState code_arm_required: bool code_format: CodeFormat supported_features: int - translation_key: str class BaseAlarmControlPanel(PlatformEntity, ABC): @@ -62,11 +61,15 @@ class BaseAlarmControlPanel(PlatformEntity, ABC): PLATFORM = Platform.ALARM_CONTROL_PANEL @property - def state(self) -> dict[str, Any]: + def state(self) -> AlarmControlPanelState: """Get the state of the alarm control panel.""" - response = super().state - response["state"] = self.alarm_state - return response + return AlarmControlPanelState( + **super().state.__dict__, + alarm_state=self.alarm_state, + code_arm_required=self.code_arm_required, + code_format=self.code_format, + supported_features=self.supported_features, + ) @property @abstractmethod @@ -91,16 +94,6 @@ def supported_features(self) -> int: """Return the list of supported features.""" return self._attr_supported_features - @functools.cached_property - def info_object(self) -> AlarmControlPanelEntityInfo: - """Return a representation of the alarm control panel.""" - return AlarmControlPanelEntityInfo( - **super().info_object.__dict__, - code_arm_required=self.code_arm_required, - code_format=self.code_format, - supported_features=self.supported_features, - ) - @abstractmethod async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" diff --git a/zha/application/platforms/binary_sensor/__init__.py b/zha/application/platforms/binary_sensor/__init__.py index 688cb706c..91823e13a 100644 --- a/zha/application/platforms/binary_sensor/__init__.py +++ b/zha/application/platforms/binary_sensor/__init__.py @@ -4,8 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable -from dataclasses import dataclass -import functools +import dataclasses import logging from typing import TYPE_CHECKING, Any @@ -26,7 +25,7 @@ from zha.application.helpers import safe_read from zha.application.platforms import ( AttrConfig, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, EntityCategory, @@ -54,12 +53,12 @@ _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True, kw_only=True) -class BinarySensorEntityInfo(BaseEntityInfo): - """Binary sensor entity info.""" +@dataclasses.dataclass(frozen=True, kw_only=True) +class BinarySensorState(BaseEntityState): + """State for binary sensor entities.""" + is_on: bool attribute_name: str - device_class: BinarySensorDeviceClass | None class BaseBinarySensor(PlatformEntity, ABC): @@ -67,13 +66,6 @@ class BaseBinarySensor(PlatformEntity, ABC): PLATFORM: Platform = Platform.BINARY_SENSOR - @property - def state(self) -> dict: - """Return the state of the binary sensor.""" - response = super().state - response["state"] = self.is_on - return response - @property @abstractmethod def is_on(self) -> bool: @@ -87,6 +79,15 @@ class BinarySensor(BaseBinarySensor): _attribute_name: str _attribute_converter: Callable[[Any], Any] | None = None + @property + def state(self) -> BinarySensorState: + """Return the state of the binary sensor.""" + return BinarySensorState( + **super().state.__dict__, + is_on=self.is_on, + attribute_name=self._attribute_name, + ) + def __init__( self, endpoint: Endpoint, @@ -134,14 +135,6 @@ def on_add(self) -> None: ) ) - @functools.cached_property - def info_object(self) -> BinarySensorEntityInfo: - """Return a representation of the binary sensor.""" - return BinarySensorEntityInfo( - **super().info_object.__dict__, - attribute_name=self._attribute_name, - ) - @property def is_on(self) -> bool: """Return True if the switch is on based on the state machine.""" diff --git a/zha/application/platforms/button/__init__.py b/zha/application/platforms/button/__init__.py index ec664037c..03eb39aee 100644 --- a/zha/application/platforms/button/__init__.py +++ b/zha/application/platforms/button/__init__.py @@ -14,7 +14,7 @@ from zha.application.helpers import write_attributes_safe from zha.application.platforms import ( BaseEntity, - BaseEntityInfo, + BaseEntityState, ClusterMatch, EntityCategory, PlatformEntity, @@ -32,13 +32,15 @@ @dataclass(frozen=True, kw_only=True) -class ButtonEntityInfo(BaseEntityInfo): - """Button entity info.""" +class ButtonState(BaseEntityState): + """State for button entities.""" + + pass @dataclass(frozen=True, kw_only=True) -class CommandButtonEntityInfo(ButtonEntityInfo): - """Command button entity info.""" +class CommandButtonState(ButtonState): + """State for command button entities.""" command: str args: list[Any] @@ -46,8 +48,8 @@ class CommandButtonEntityInfo(ButtonEntityInfo): @dataclass(frozen=True, kw_only=True) -class WriteAttributeButtonEntityInfo(ButtonEntityInfo): - """Write attribute button entity info.""" +class WriteAttributeButtonState(ButtonState): + """State for write attribute button entities.""" attribute_name: str attribute_value: Any @@ -58,10 +60,10 @@ class BaseButton(PlatformEntity, ABC): PLATFORM = Platform.BUTTON - @functools.cached_property - def info_object(self) -> ButtonEntityInfo: - """Return a representation of the button.""" - return ButtonEntityInfo(**super().info_object.__dict__) + @property + def state(self) -> ButtonState: + """Return the state of the button.""" + return ButtonState(**super().state.__dict__) @abstractmethod async def async_press(self) -> None: @@ -81,7 +83,7 @@ def __init__( device: Device, *, command_name: str | None = None, - command_args: tuple | None = None, + command_args: list[Any] | None = None, command_kwargs: dict[str, Any] | None = None, **kwargs: Any, ) -> None: @@ -95,11 +97,11 @@ def __init__( super().__init__(endpoint=endpoint, device=device, **kwargs) - @functools.cached_property - def info_object(self) -> CommandButtonEntityInfo: - """Return a representation of the button.""" - return CommandButtonEntityInfo( - **super().info_object.__dict__, + @property + def state(self) -> CommandButtonState: + """Return the state of the button.""" + return CommandButtonState( + **super().state.__dict__, command=self._command_name, args=self._args, kwargs=self._kwargs, @@ -168,11 +170,11 @@ def __init__( super().__init__(endpoint=endpoint, device=device, **kwargs) self.recompute_capabilities() - @functools.cached_property - def info_object(self) -> WriteAttributeButtonEntityInfo: - """Return a representation of the button.""" - return WriteAttributeButtonEntityInfo( - **super().info_object.__dict__, + @property + def state(self) -> WriteAttributeButtonState: + """Return the state of the button.""" + return WriteAttributeButtonState( + **super().state.__dict__, attribute_name=self._attribute_name, attribute_value=self._attribute_value, ) diff --git a/zha/application/platforms/climate/__init__.py b/zha/application/platforms/climate/__init__.py index 6e5d9a27a..31acebd1b 100644 --- a/zha/application/platforms/climate/__init__.py +++ b/zha/application/platforms/climate/__init__.py @@ -7,7 +7,7 @@ from dataclasses import dataclass import datetime as dt import functools -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from zigpy.profiles import zha from zigpy.zcl import ( @@ -29,7 +29,7 @@ from zha.application.helpers import write_attributes_safe from zha.application.platforms import ( AttrConfig, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, PlatformEntity, @@ -39,12 +39,6 @@ from zha.application.platforms.climate.const import ( ATTR_OCCP_COOL_SETPT, ATTR_OCCP_HEAT_SETPT, - ATTR_OCCUPANCY, - ATTR_PI_COOLING_DEMAND, - ATTR_PI_HEATING_DEMAND, - ATTR_SYS_MODE, - ATTR_UNOCCP_COOL_SETPT, - ATTR_UNOCCP_HEAT_SETPT, FAN_AUTO, FAN_ON, HVAC_MODE_2_SYSTEM, @@ -66,9 +60,18 @@ @dataclass(frozen=True, kw_only=True) -class ThermostatEntityInfo(BaseEntityInfo): - """Thermostat entity info.""" - +class ClimateState(BaseEntityState): + """State for climate entities.""" + + current_temperature: float | None + outdoor_temperature: float | None + target_temperature: float | None + target_temperature_high: float | None + target_temperature_low: float | None + hvac_action: HVACAction | None + hvac_mode: HVACMode | None + preset_mode: str | None + fan_mode: str | None max_temp: float min_temp: float supported_features: ClimateEntityFeature @@ -77,25 +80,46 @@ class ThermostatEntityInfo(BaseEntityInfo): hvac_modes: list[HVACMode] +@dataclass(frozen=True, kw_only=True) +class ThermostatState(ClimateState): + """State for thermostat entities with extra attributes.""" + + sys_mode: str | None = None + occupancy: int | None = None + occupied_cooling_setpoint: int | None = None + occupied_heating_setpoint: int | None = None + pi_heating_demand: int | None = None + pi_cooling_demand: int | None = None + unoccupied_cooling_setpoint: int | None = None + unoccupied_heating_setpoint: int | None = None + + class BaseThermostat(PlatformEntity, ABC): """Abstract base class for climate entities.""" PLATFORM = Platform.CLIMATE @property - def state(self) -> dict[str, Any]: + def state(self) -> ClimateState: """Get the state of the climate entity.""" - response = super().state - response["current_temperature"] = self.current_temperature - response["outdoor_temperature"] = self.outdoor_temperature - response["target_temperature"] = self.target_temperature - response["target_temperature_high"] = self.target_temperature_high - response["target_temperature_low"] = self.target_temperature_low - response["hvac_action"] = self.hvac_action - response["hvac_mode"] = self.hvac_mode - response["preset_mode"] = self.preset_mode - response["fan_mode"] = self.fan_mode - return response + return ClimateState( + **super().state.__dict__, + current_temperature=self.current_temperature, + outdoor_temperature=self.outdoor_temperature, + target_temperature=self.target_temperature, + target_temperature_high=self.target_temperature_high, + target_temperature_low=self.target_temperature_low, + hvac_action=self.hvac_action, + hvac_mode=self.hvac_mode, + preset_mode=self.preset_mode, + fan_mode=self.fan_mode, + max_temp=self.max_temp, + min_temp=self.min_temp, + supported_features=self.supported_features, + fan_modes=self.fan_modes, + preset_modes=self.preset_modes, + hvac_modes=self.hvac_modes, + ) @property @abstractmethod @@ -207,16 +231,6 @@ class Thermostat(BaseThermostat): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key: str = "thermostat" _enable_turn_on_off_backwards_compatibility = False - _attr_extra_state_attribute_names: set[str] = { - ATTR_SYS_MODE, - ATTR_OCCUPANCY, - ATTR_OCCP_COOL_SETPT, - ATTR_OCCP_HEAT_SETPT, - ATTR_PI_HEATING_DEMAND, - ATTR_PI_COOLING_DEMAND, - ATTR_UNOCCP_COOL_SETPT, - ATTR_UNOCCP_HEAT_SETPT, - } _cluster_match = ClusterMatch( server_clusters=frozenset({ThermostatCluster.cluster_id}), @@ -544,39 +558,28 @@ def on_add(self) -> None: ) ) - @functools.cached_property - def info_object(self) -> ThermostatEntityInfo: - """Return a representation of the thermostat.""" - return ThermostatEntityInfo( - **super().info_object.__dict__, - max_temp=self.max_temp, - min_temp=self.min_temp, - supported_features=self.supported_features, - fan_modes=self.fan_modes, - preset_modes=self.preset_modes, - hvac_modes=self.hvac_modes, + @property + def state(self) -> ThermostatState: + """Get the state of the thermostat.""" + return ThermostatState( + **super().state.__dict__, + sys_mode=self.system_mode_string, + occupancy=self._occupancy, + occupied_cooling_setpoint=self._occupied_cooling_setpoint, + occupied_heating_setpoint=self._occupied_heating_setpoint, + pi_heating_demand=self._pi_heating_demand, + pi_cooling_demand=self._pi_cooling_demand, + unoccupied_cooling_setpoint=self._unoccupied_cooling_setpoint, + unoccupied_heating_setpoint=self._unoccupied_heating_setpoint, ) @property - def state(self) -> dict[str, Any]: - """Get the state of the thermostat.""" + def system_mode_string(self) -> str | None: + """Return a formatted system mode string.""" + if self.hvac_mode is None: + return None system_mode = SYSTEM_MODE_2_HVAC.get(self._system_mode, "unknown") - - response = super().state - - response[ATTR_SYS_MODE] = ( - f"[{self._system_mode}]/{system_mode}" - if self.hvac_mode is not None - else None - ) - response[ATTR_OCCUPANCY] = self._occupancy - response[ATTR_OCCP_COOL_SETPT] = self._occupied_cooling_setpoint - response[ATTR_OCCP_HEAT_SETPT] = self._occupied_heating_setpoint - response[ATTR_PI_HEATING_DEMAND] = self._pi_heating_demand - response[ATTR_PI_COOLING_DEMAND] = self._pi_cooling_demand - response[ATTR_UNOCCP_COOL_SETPT] = self._unoccupied_cooling_setpoint - response[ATTR_UNOCCP_HEAT_SETPT] = self._unoccupied_heating_setpoint - return response + return f"[{self._system_mode}]/{system_mode}" @property def current_temperature(self): @@ -1096,21 +1099,14 @@ def current_temperature(self): return None @property - def state(self) -> dict[str, Any]: - """Get the state of the lock.""" + def system_mode_string(self) -> str | None: + """Return a formatted system mode string.""" + if self.hvac_mode is None: + return None system_mode = ZehnderThermostat.ZEHNDER_SYSTEM_MODE_2_HVAC.get( self._system_mode, "unknown" ) - - response = super().state - - response[ATTR_SYS_MODE] = ( - f"[{self._system_mode}]/{system_mode}" - if self.hvac_mode is not None - else None - ) - - return response + return f"[{self._system_mode}]/{system_mode}" @property def hvac_mode(self) -> HVACMode | None: diff --git a/zha/application/platforms/cover/__init__.py b/zha/application/platforms/cover/__init__.py index 8f6385e2b..7256db53c 100644 --- a/zha/application/platforms/cover/__init__.py +++ b/zha/application/platforms/cover/__init__.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod import asyncio from collections import deque +import dataclasses import functools import logging from typing import TYPE_CHECKING, Any @@ -31,6 +32,7 @@ from zha.application.helpers import safe_read from zha.application.platforms import ( AttrConfig, + BaseEntityState, ClusterConfig, ClusterMatch, PlatformEntity, @@ -38,8 +40,6 @@ register_entity, ) from zha.application.platforms.cover.const import ( - ATTR_CURRENT_POSITION, - ATTR_CURRENT_TILT_POSITION, POSITION_CLOSED, POSITION_OPEN, WCT, @@ -65,6 +65,18 @@ TILT_MOVEMENT_TIMEOUT_RANGE: float = 30 +@dataclasses.dataclass(frozen=True, kw_only=True) +class CoverEntityState(BaseEntityState): + """State for cover entities.""" + + current_position: int | None + current_tilt_position: int | None + is_opening: bool | None + is_closing: bool | None + is_closed: bool | None + supported_features: CoverEntityFeature + + class BaseCover(PlatformEntity, ABC): """Abstract base class for ZHA covers.""" @@ -290,21 +302,17 @@ def restore_external_state_attributes( ) @property - def state(self) -> dict[str, Any]: + def state(self) -> CoverEntityState: """Get the state of the cover.""" - response = super().state - response.update( - { - ATTR_CURRENT_POSITION: self.current_cover_position, - ATTR_CURRENT_TILT_POSITION: self.current_cover_tilt_position, - "state": self._state, - "is_opening": self.is_opening, - "is_closing": self.is_closing, - "is_closed": self.is_closed, - "supported_features": self.supported_features, - } + return CoverEntityState( + **super().state.__dict__, + current_position=self.current_cover_position, + current_tilt_position=self.current_cover_tilt_position, + is_opening=self.is_opening, + is_closing=self.is_closing, + is_closed=self.is_closed, + supported_features=self.supported_features, ) - return response @property def is_closed(self) -> bool | None: @@ -883,21 +891,17 @@ def on_add(self) -> None: ) @property - def state(self) -> dict[str, Any]: + def state(self) -> CoverEntityState: """Get the state of the cover.""" - if (closed := self.is_closed) is None: - state = None - else: - state = CoverState.CLOSED if closed else CoverState.OPEN - response = super().state - response.update( - { - ATTR_CURRENT_POSITION: self.current_cover_position, - "is_closed": self.is_closed, - "state": state, - } + return CoverEntityState( + **super().state.__dict__, + current_position=self.current_cover_position, + current_tilt_position=self.current_cover_tilt_position, + is_opening=self.is_opening, + is_closing=self.is_closing, + is_closed=self.is_closed, + supported_features=self.supported_features, ) - return response @property def current_cover_position(self) -> int | None: diff --git a/zha/application/platforms/device_tracker.py b/zha/application/platforms/device_tracker.py index 0328195e0..f283f621e 100644 --- a/zha/application/platforms/device_tracker.py +++ b/zha/application/platforms/device_tracker.py @@ -3,10 +3,11 @@ from __future__ import annotations from abc import ABC, abstractmethod +import dataclasses from enum import StrEnum import functools import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from zigpy.profiles import zha from zigpy.zcl import ( @@ -21,6 +22,7 @@ from zha.application import Platform from zha.application.platforms import ( AttrConfig, + BaseEntityState, ClusterConfig, ClusterMatch, PlatformEntity, @@ -48,22 +50,27 @@ class SourceType(StrEnum): BLUETOOTH_LE = "bluetooth_le" +@dataclasses.dataclass(frozen=True, kw_only=True) +class DeviceTrackerState(BaseEntityState): + """State for device tracker entities.""" + + connected: bool + battery_level: float | None + + class BaseDeviceTracker(PlatformEntity, ABC): """Abstract base class for ZHA device tracker entities.""" PLATFORM = Platform.DEVICE_TRACKER @property - def state(self) -> dict[str, Any]: + def state(self) -> DeviceTrackerState: """Return the state of the device.""" - response = super().state - response.update( - { - "connected": self.is_connected, - "battery_level": self.battery_level, - } + return DeviceTrackerState( + **super().state.__dict__, + connected=self.is_connected, + battery_level=self.battery_level, ) - return response @property @abstractmethod diff --git a/zha/application/platforms/fan/__init__.py b/zha/application/platforms/fan/__init__.py index 5f60e4663..ed37f19dd 100644 --- a/zha/application/platforms/fan/__init__.py +++ b/zha/application/platforms/fan/__init__.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import functools import math -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from zigpy.zcl import ( AttributeReadEvent, @@ -22,7 +22,7 @@ from zha.application.platforms import ( AttrConfig, BaseEntity, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, GroupEntity, @@ -33,8 +33,6 @@ ) from zha.application.platforms.const import IKEA_AIR_PURIFIER_CLUSTER from zha.application.platforms.fan.const import ( - ATTR_PERCENTAGE, - ATTR_PRESET_MODE, DEFAULT_ON_PERCENTAGE, LEGACY_SPEED_LIST, OFF_SPEED_VALUES, @@ -61,13 +59,19 @@ @dataclass(frozen=True, kw_only=True) -class FanEntityInfo(BaseEntityInfo): - """Fan entity info.""" +class FanState(BaseEntityState): + """State for fan entities.""" + preset_mode: str | None + percentage: int | None + is_on: bool + speed: str | None preset_modes: list[str] supported_features: FanEntityFeature speed_count: int speed_list: list[str] + speed_range: tuple[int, int] + default_on_percentage: int class BaseFan(BaseEntity, ABC): @@ -155,31 +159,23 @@ def speed(self) -> str | None: return None return self.percentage_to_speed(percentage) - @functools.cached_property - def info_object(self) -> FanEntityInfo: - """Return a representation of the binary sensor.""" - return FanEntityInfo( - **super().info_object.__dict__, + @property + def state(self) -> FanState: + """Return the state of the fan.""" + return FanState( + **super().state.__dict__, + preset_mode=self.preset_mode, + percentage=self.percentage, + is_on=self.is_on, + speed=self.speed, preset_modes=self.preset_modes, supported_features=self.supported_features, speed_count=self.speed_count, speed_list=self.speed_list, + speed_range=self.speed_range, + default_on_percentage=self.default_on_percentage, ) - @property - def state(self) -> dict[str, Any]: - """Return the state of the fan.""" - response = super().state - response.update( - { - "preset_mode": self.preset_mode, - "percentage": self.percentage, - "is_on": self.is_on, - "speed": self.speed, - } - ) - return response - async def async_turn_on( self, speed: str | None = None, @@ -344,10 +340,8 @@ def __init__(self, group: Group): """Initialize a fan group.""" self._cluster = group.endpoint[hvac.Fan.cluster_id] super().__init__(group) - self._percentage = None - self._preset_mode = None - if hasattr(self, "info_object"): - delattr(self, "info_object") + self._percentage: int | None = None + self._preset_mode: str | None = None self.update() @property @@ -369,23 +363,19 @@ def update(self, _: Any = None) -> None: """Query all members and determine the fan group state.""" self.debug("Updating fan group entity state") platform_entities = self._group.get_platform_entities(self.PLATFORM) - all_states = [entity.state for entity in platform_entities] + all_states = [cast(FanState, entity.state) for entity in platform_entities] self.debug( "All platform entity states for group entity members: %s", all_states ) - percentage_states: list[dict] = [ - state for state in all_states if state.get(ATTR_PERCENTAGE) - ] - preset_mode_states: list[dict] = [ - state for state in all_states if state.get(ATTR_PRESET_MODE) - ] + percentage_states = [state for state in all_states if state.percentage] + preset_mode_states = [state for state in all_states if state.preset_mode] if percentage_states: - self._percentage = percentage_states[0][ATTR_PERCENTAGE] + self._percentage = percentage_states[0].percentage self._preset_mode = None elif preset_mode_states: - self._preset_mode = preset_mode_states[0][ATTR_PRESET_MODE] + self._preset_mode = preset_mode_states[0].preset_mode self._percentage = None else: self._percentage = None diff --git a/zha/application/platforms/helpers.py b/zha/application/platforms/helpers.py index 2872d8339..ed98441b4 100644 --- a/zha/application/platforms/helpers.py +++ b/zha/application/platforms/helpers.py @@ -7,11 +7,13 @@ import logging from typing import Any +from zha.application.platforms import BaseEntityState -def find_state_attributes(states: list[dict], key: str) -> Iterator[Any]: + +def find_state_attributes(states: list[BaseEntityState], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: - if (value := state.get(key)) is not None: + if (value := getattr(state, key, None)) is not None: yield value @@ -26,7 +28,7 @@ def mean_tuple(*args: Any) -> tuple: def reduce_attribute( - states: list[dict], + states: list[BaseEntityState], key: str, default: Any | None = None, reduce: Callable[..., Any] = mean_int, diff --git a/zha/application/platforms/light/__init__.py b/zha/application/platforms/light/__init__.py index c6f1249f4..1f637fac7 100644 --- a/zha/application/platforms/light/__init__.py +++ b/zha/application/platforms/light/__init__.py @@ -9,8 +9,6 @@ from collections import Counter import contextlib import dataclasses -from dataclasses import dataclass -import functools import itertools import logging from typing import TYPE_CHECKING, Any @@ -32,7 +30,7 @@ DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY, AttrConfig, BaseEntity, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, GroupEntity, @@ -88,24 +86,29 @@ _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True, kw_only=True) -class LightEntityInfo(BaseEntityInfo): - """Light entity info.""" +@dataclasses.dataclass(frozen=True, kw_only=True) +class LightState(BaseEntityState): + """State for light entities.""" - effect_list: list[str] | None = dataclasses.field(default=None) + on: bool | None + brightness: int | None + xy_color: tuple[float, float] | None + color_temp: int | None + effect_list: list[str] | None + effect: str supported_features: LightEntityFeature - min_mireds: int - max_mireds: int + color_mode: ColorMode | None + supported_color_modes: set[ColorMode] + off_with_transition: bool + off_brightness: int | None + min_mireds: int | None + max_mireds: int | None class BaseLight(BaseEntity, ABC): """Operations common to all light entities.""" PLATFORM = Platform.LIGHT - _attr_extra_state_attribute_names: set[str] = { - "off_with_transition", - "off_brightness", - } _attr_primary_weight = 10 def __init__(self, *args, **kwargs): @@ -126,29 +129,21 @@ def __init__(self, *args, **kwargs): self._supported_color_modes: set[ColorMode] = set() @property - def state(self) -> dict[str, Any]: + def state(self) -> LightState: """Return the state of the light.""" - response = super().state - response["on"] = self.is_on - response["brightness"] = self.brightness - response["xy_color"] = self.xy_color - response["color_temp"] = self.color_temp - response["effect_list"] = self.effect_list - response["effect"] = self.effect - response["supported_features"] = self.supported_features - response["color_mode"] = self.color_mode - response["supported_color_modes"] = self.supported_color_modes - response["off_with_transition"] = self._off_with_transition - response["off_brightness"] = self._off_brightness - return response - - @functools.cached_property - def info_object(self) -> LightEntityInfo: - """Return a representation of the select.""" - return LightEntityInfo( - **super().info_object.__dict__, + return LightState( + **super().state.__dict__, + on=self.is_on, + brightness=self.brightness, + xy_color=self.xy_color, + color_temp=self.color_temp, effect_list=self.effect_list, + effect=self.effect, supported_features=self.supported_features, + color_mode=self.color_mode, + supported_color_modes=self.supported_color_modes, + off_with_transition=self._off_with_transition, + off_brightness=self._off_brightness, min_mireds=self.min_mireds, max_mireds=self.max_mireds, ) @@ -357,12 +352,12 @@ def _gateway(self) -> Gateway: """Return the gateway.""" @property - def state(self) -> dict[str, Any]: + def state(self) -> LightState: """Return the state of the light.""" - response = super().state - # XXX: for backwards compatibility - response["supported_color_modes"] = self._internal_supported_color_modes - return response + return dataclasses.replace( + super().state, + supported_color_modes=self._internal_supported_color_modes, + ) def recompute_capabilities(self) -> None: """Recompute supported features and color modes.""" @@ -1431,8 +1426,6 @@ def recompute_capabilities(self) -> None: self._color_mode = ColorMode.UNKNOWN self._internal_supported_color_modes = {ColorMode.ONOFF} - if hasattr(self, "info_object"): - delattr(self, "info_object") self.update() async def on_remove(self) -> None: @@ -1476,7 +1469,7 @@ def update(self, _: Any = None) -> None: self.debug( "All platform entity states for group entity members: %s", all_states ) - on_states = [state for state in states if state["on"]] + on_states = [state for state in states if state.on] self._state = len(on_states) > 0 diff --git a/zha/application/platforms/lock/__init__.py b/zha/application/platforms/lock/__init__.py index 563e51b99..be4e8b3fc 100644 --- a/zha/application/platforms/lock/__init__.py +++ b/zha/application/platforms/lock/__init__.py @@ -3,7 +3,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Literal +import dataclasses +from typing import TYPE_CHECKING, Any, Literal, cast from zigpy.zcl import ( AttributeReadEvent, @@ -12,13 +13,17 @@ AttributeWrittenEvent, ReportingConfig, ) -from zigpy.zcl.clusters.closures import DoorLock as DoorLockCluster +from zigpy.zcl.clusters.closures import ( + DoorLock as DoorLockCluster, + LockState as ZclLockState, +) from zigpy.zcl.foundation import Status from zha.application import Platform from zha.application.helpers import safe_read from zha.application.platforms import ( AttrConfig, + BaseEntityState, ClusterConfig, ClusterMatch, PlatformEntity, @@ -35,17 +40,25 @@ from zha.zigbee.endpoint import Endpoint +@dataclasses.dataclass(frozen=True, kw_only=True) +class LockState(BaseEntityState): + """State for lock entities.""" + + is_locked: bool + + class BaseLock(PlatformEntity, ABC): """Abstract base class for ZHA lock entities.""" PLATFORM = Platform.LOCK @property - def state(self) -> dict[str, Any]: + def state(self) -> LockState: """Get the state of the lock.""" - response = super().state - response["is_locked"] = self.is_locked - return response + return LockState( + **super().state.__dict__, + is_locked=self.is_locked, + ) @property @abstractmethod @@ -90,7 +103,7 @@ def __init__( self, endpoint: Endpoint, device: Device, - **kwargs, + **kwargs: Any, ) -> None: """Initialize the lock.""" super().__init__(endpoint=endpoint, device=device, **kwargs) @@ -179,7 +192,9 @@ def handle_attribute_updated( """Handle state update from the door-lock cluster.""" if event.attribute_id != DoorLockCluster.AttributeDefs.lock_state.id: return - self._state = VALUE_TO_STATE.get(event.value, self._state) + + value = cast(ZclLockState, event.value) + self._state = VALUE_TO_STATE.get(value, self._state) self.maybe_emit_state_changed_event() async def async_update(self) -> None: diff --git a/zha/application/platforms/number/__init__.py b/zha/application/platforms/number/__init__.py index 40f4f8e84..31d67af9d 100644 --- a/zha/application/platforms/number/__init__.py +++ b/zha/application/platforms/number/__init__.py @@ -24,7 +24,7 @@ from zha.application.helpers import safe_read, write_attributes_safe from zha.application.platforms import ( AttrConfig, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, EntityCategory, @@ -53,9 +53,10 @@ @dataclass(frozen=True, kw_only=True) -class NumberEntityInfo(BaseEntityInfo): - """Number entity info.""" +class NumberState(BaseEntityState): + """State for number entities.""" + native_value: float | None mode: NumberMode native_max_value: float native_min_value: float @@ -82,11 +83,12 @@ class BaseNumber(PlatformEntity, ABC): def native_value(self) -> float | None: """Return the current value.""" - @functools.cached_property - def info_object(self) -> NumberEntityInfo: - """Return a representation of the number entity.""" - return NumberEntityInfo( - **super().info_object.__dict__, + @property + def state(self) -> NumberState: + """Return the state of the entity.""" + return NumberState( + **super().state.__dict__, + native_value=self.native_value, mode=self.mode, native_max_value=self.native_max_value, native_min_value=self.native_min_value, @@ -94,13 +96,6 @@ def info_object(self) -> NumberEntityInfo: native_unit_of_measurement=self.native_unit_of_measurement, ) - @property - def state(self) -> dict[str, Any]: - """Return the state of the entity.""" - response = super().state - response["state"] = self.native_value - return response - @property def native_min_value(self) -> float: """Return the minimum value.""" @@ -331,13 +326,6 @@ def on_add(self) -> None: ) ) - @property - def state(self) -> dict[str, Any]: - """Return the state of the entity.""" - response = super().state - response["state"] = self.native_value - return response - @property def native_value(self) -> float | None: """Return the current value.""" diff --git a/zha/application/platforms/select.py b/zha/application/platforms/select.py index 5131bf6c6..2f6fd932d 100644 --- a/zha/application/platforms/select.py +++ b/zha/application/platforms/select.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum -import functools import logging from typing import TYPE_CHECKING, Any, cast @@ -33,7 +32,7 @@ from zha.application.platforms import ( AttrConfig, BaseEntity, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, EntityCategory, @@ -70,8 +69,15 @@ @dataclass(frozen=True, kw_only=True) -class EnumSelectInfo(BaseEntityInfo): - """Enum select entity info.""" +class SelectState(BaseEntityState): + """State for select entities.""" + + current_option: str | None + + +@dataclass(frozen=True, kw_only=True) +class EnumSelectState(SelectState): + """State for enum select entities.""" enum: str options: list[str] @@ -90,11 +96,12 @@ def options(self) -> list[str]: return self._attr_options @property - def state(self) -> dict[str, Any]: + def state(self) -> SelectState: """Return the state of the select.""" - response = super().state - response["state"] = self.current_option - return response + return SelectState( + **super().state.__dict__, + current_option=self.current_option, + ) @property @abstractmethod @@ -132,11 +139,11 @@ def __init__( self._attr_options = list(self._option_to_member) super().__init__(endpoint=endpoint, device=device, **kwargs) - @functools.cached_property - def info_object(self) -> EnumSelectInfo: - """Return a representation of the select.""" - return EnumSelectInfo( - **super().info_object.__dict__, + @property + def state(self) -> EnumSelectState: + """Return the state of the select.""" + return EnumSelectState( + **super().state.__dict__, enum=self._enum.__name__, options=self.options, ) @@ -295,11 +302,11 @@ def _is_supported(self) -> bool: return super()._is_supported() - @functools.cached_property - def info_object(self) -> EnumSelectInfo: - """Return a representation of the select.""" - return EnumSelectInfo( - **super().info_object.__dict__, + @property + def state(self) -> EnumSelectState: + """Return the state of the select.""" + return EnumSelectState( + **super().state.__dict__, enum=self._enum.__name__, options=self.options, ) diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index 5ac490b1b..235a9e593 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -60,7 +60,7 @@ from zha.application.platforms import ( AttrConfig, BaseEntity, - BaseEntityInfo, + BaseEntityState, BaseIdentifiers, ClusterConfig, ClusterMatch, @@ -164,22 +164,20 @@ @dataclass(frozen=True, kw_only=True) -class SensorEntityInfo(BaseEntityInfo): - """Sensor entity info.""" +class SensorState(BaseEntityState): + """State for sensor entities.""" + native_value: date | datetime | str | int | float | None suggested_display_precision: int | None = None unit: str | None = None - device_class: SensorDeviceClass | None = None - state_class: SensorStateClass | None = None @dataclass(frozen=True, kw_only=True) -class DeviceCounterEntityInfo(BaseEntityInfo): - """Device counter entity info.""" +class DeviceCounterSensorState(BaseEntityState): + """State for device counter sensor entities.""" - device_ieee: str + native_value: int | None suggested_display_precision: int - available: bool counter: str counter_value: int counter_groups: str @@ -213,22 +211,16 @@ def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self._attr_native_unit_of_measurement - @functools.cached_property - def info_object(self) -> SensorEntityInfo: - """Return a representation of the sensor.""" - return SensorEntityInfo( - **super().info_object.__dict__, + @property + def state(self) -> SensorState: + """Return the state for this sensor.""" + return SensorState( + **super().state.__dict__, + native_value=self.native_value, suggested_display_precision=self.suggested_display_precision, unit=self.native_unit_of_measurement, ) - @property - def state(self) -> dict: - """Return the state for this sensor.""" - response = super().state - response["state"] = self.native_value - return response - @property @abstractmethod def native_value(self) -> date | datetime | str | int | float | None: @@ -344,6 +336,8 @@ def _validate_state_class( def native_value(self) -> date | datetime | str | int | float | None: """Return the state of the entity.""" assert self._attribute_name is not None + if self._attribute_name not in self._cluster.attributes_by_name: + return None raw_state = self._cluster.get(self._attribute_name) if raw_state is None: return None @@ -364,9 +358,8 @@ def handle_attribute_updated( if ( event.attribute_name == self._attribute_name or ( - hasattr(self, "_attr_extra_state_attribute_names") - and event.attribute_name - in getattr(self, "_attr_extra_state_attribute_names") + self._attr_extra_state_attribute_names is not None + and event.attribute_name in self._attr_extra_state_attribute_names ) or self._attribute_name is None ): @@ -471,11 +464,12 @@ def identifiers(self) -> DeviceCounterSensorIdentifiers: **super().identifiers.__dict__, device_ieee=str(self._device.ieee) ) - @functools.cached_property - def info_object(self) -> DeviceCounterEntityInfo: - """Return a representation of the platform entity.""" - return DeviceCounterEntityInfo( - **super().info_object.__dict__, + @property + def state(self) -> DeviceCounterSensorState: + """Return the state for this sensor.""" + return DeviceCounterSensorState( + **super().state.__dict__, + native_value=self._zigpy_counter.value, suggested_display_precision=self._attr_suggested_display_precision, counter=self._zigpy_counter.name, counter_value=self._zigpy_counter.value, @@ -483,13 +477,6 @@ def info_object(self) -> DeviceCounterEntityInfo: counter_group=self._zigpy_counter_group, ) - @property - def state(self) -> dict[str, Any]: - """Return the state for this sensor.""" - response = super().state - response["state"] = self._zigpy_counter.value - return response - @property def native_value(self) -> int | None: """Return the state of the entity.""" @@ -722,6 +709,15 @@ def _is_supported(self) -> bool: return super()._is_supported() +@dataclass(frozen=True, kw_only=True) +class BatterySensorState(SensorState): + """State for battery sensor entities.""" + + battery_size: str | None = None + battery_quantity: int | None = None + battery_voltage: float | None = None + + @register_entity(PowerConfiguration.cluster_id) class Battery(Sensor): """Battery sensor of power configuration cluster.""" @@ -782,19 +778,36 @@ def formatter(value: int) -> float | None: # pylint: disable=arguments-differ return value / 2 @property - def state(self) -> dict[str, Any]: + def state(self) -> BatterySensorState: """Return the state for battery sensors.""" - response = super().state - battery_size = self._cluster.get("battery_size") - if battery_size is not None: - response["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") + battery_size_raw = self._cluster.get("battery_size") + battery_size = ( + BATTERY_SIZES.get(battery_size_raw, "Unknown") + if battery_size_raw is not None + else None + ) battery_quantity = self._cluster.get("battery_quantity") - if battery_quantity is not None: - response["battery_quantity"] = battery_quantity - battery_voltage = self._cluster.get("battery_voltage") - if battery_voltage is not None: - response["battery_voltage"] = round(battery_voltage / 10, 2) - return response + battery_voltage_raw = self._cluster.get("battery_voltage") + battery_voltage = ( + round(battery_voltage_raw / 10, 2) + if battery_voltage_raw is not None + else None + ) + return BatterySensorState( + **super().state.__dict__, + battery_size=battery_size, + battery_quantity=battery_quantity, + battery_voltage=battery_voltage, + ) + + +@dataclass(frozen=True, kw_only=True) +class ElectricalMeasurementState(SensorState): + """State for electrical measurement sensor entities.""" + + measurement_type: str | None = None + max_value: float | int | None = None + max_attribute_name: str | None = None class BaseElectricalMeasurement(Sensor): @@ -837,20 +850,19 @@ def _measurement_type(self) -> str | None: ) @property - def state(self) -> dict[str, Any]: + def state(self) -> ElectricalMeasurementState: """Return the state for this sensor.""" - response = super().state - meas_type = self._measurement_type - if meas_type is not None: - response["measurement_type"] = meas_type - - if (max_attr_name := self._attr_max_attribute_name) is None: - return response - - if (max_v := self._cluster.get(max_attr_name)) is not None: - response[max_attr_name] = self.formatter(max_v) - - return response + max_value = None + if (max_attr_name := self._attr_max_attribute_name) is not None: + max_v = self._cluster.get(max_attr_name) + if max_v is not None: + max_value = self.formatter(max_v) + return ElectricalMeasurementState( + **super().state.__dict__, + measurement_type=self._measurement_type, + max_value=max_value, + max_attribute_name=self._attr_max_attribute_name, + ) @property def _multiplier(self) -> int | float | None: @@ -2087,6 +2099,15 @@ class SmartEnergyMeteringEntityDescription: device_class: SensorDeviceClass | None = None +@dataclass(frozen=True, kw_only=True) +class SmartEnergySensorState(SensorState): + """State for smart energy metering sensor entities.""" + + device_type: int | None = None + status: str | None = None + zcl_unit_of_measurement: int | None = None + + @register_entity(Metering.cluster_id) class MeteringPoller(AggregatedClusterPoller): """Polls the Metering cluster for models known to need polling. @@ -2323,20 +2344,22 @@ def recompute_capabilities(self) -> None: ) @property - def state(self) -> dict[str, Any]: + def state(self) -> SmartEnergySensorState: """Return state for this sensor.""" - response = super().state - if self._device_type is not None: - response["device_type"] = self._device_type - if (status := self._metering_status) is not None: - if isinstance(status, enum.IntFlag): - response["status"] = str( - status.name if status.name is not None else status.value + status = None + if (raw_status := self._metering_status) is not None: + if isinstance(raw_status, enum.IntFlag): + status = str( + raw_status.name if raw_status.name is not None else raw_status.value ) else: - response["status"] = str(status)[len(status.__class__.__name__) + 1 :] - response["zcl_unit_of_measurement"] = self._unit_of_measurement - return response + status = str(raw_status)[len(raw_status.__class__.__name__) + 1 :] + return SmartEnergySensorState( + **super().state.__dict__, + device_type=self._device_type, + status=status, + zcl_unit_of_measurement=self._unit_of_measurement, + ) @property def _multiplier(self) -> int | float | None: @@ -3092,16 +3115,6 @@ def _running_mode(self) -> int | None: def _system_mode(self) -> int | None: return self._cluster.get(Thermostat.AttributeDefs.system_mode.name) - @property - def state(self) -> dict: - """Return the current HVAC action.""" - response = super().state - if self._pi_heating_demand is None and self._pi_cooling_demand is None: - response["state"] = self._rm_rs_action - else: - response["state"] = self._pi_demand_action - return response - @property def native_value(self) -> str | None: """Return the current HVAC action.""" @@ -3242,13 +3255,6 @@ def is_supported_in_list(self, entities: list[BaseEntity]) -> bool: cls = type(self) return not any(type(entity) is cls for entity in entities if entity is not self) - @property - def state(self) -> dict: - """Return the state of the sensor.""" - response = super().state - response["state"] = self.device.device.rssi - return response - @property def native_value(self) -> str | int | float | None: """Return the state of the entity.""" @@ -3292,13 +3298,6 @@ class LQISensor(RSSISensor): server_clusters=frozenset({Basic.cluster_id}), ) - @property - def state(self) -> dict: - """Return the state of the sensor.""" - response = super().state - response["state"] = self.device.device.lqi - return response - @property def native_value(self) -> str | int | float | None: """Return the state of the entity.""" @@ -3620,6 +3619,13 @@ class AqaraCurtainHookStateSensor(EnumSensor): ) +@dataclass(frozen=True, kw_only=True) +class BitmapSensorState(SensorState): + """State for bitmap sensor entities.""" + + bit_states: dict[str, bool] + + class BitMapSensor(Sensor): """A sensor with only state attributes. @@ -3641,17 +3647,19 @@ def __init__( } @property - def state(self) -> dict[str, Any]: + def state(self) -> BitmapSensorState: """Return the state for this sensor.""" - response = super().state - response["state"] = self.native_value value = self._cluster.get(self._attribute_name) + bit_states = {} for bit in list(self._bitmap): if value is None: - response[bit.name] = False + bit_states[bit.name] = False else: - response[bit.name] = bit in self._bitmap(value) - return response + bit_states[bit.name] = bit in self._bitmap(value) + return BitmapSensorState( + **super().state.__dict__, + bit_states=bit_states, + ) def formatter(self, _value: int) -> str: """Summary of all attributes.""" diff --git a/zha/application/platforms/siren.py b/zha/application/platforms/siren.py index 4a3688681..2e4579db6 100644 --- a/zha/application/platforms/siren.py +++ b/zha/application/platforms/siren.py @@ -7,7 +7,6 @@ import contextlib from dataclasses import dataclass from enum import Enum, IntFlag -import functools from typing import TYPE_CHECKING, Any, Final from zigpy.profiles import zha @@ -15,6 +14,7 @@ IasWd, SirenLevel, Squawk, + SquawkLevel, SquawkMode, Strobe, StrobeLevel, @@ -24,7 +24,7 @@ from zha.application import Platform from zha.application.platforms import ( - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, PlatformEntity, @@ -57,9 +57,10 @@ class SirenEntityFeature(IntFlag): @dataclass(frozen=True, kw_only=True) -class SirenEntityInfo(BaseEntityInfo): - """Siren entity info.""" +class SirenState(BaseEntityState): + """State for siren entities.""" + is_on: bool available_tones: dict[int, str] supported_features: SirenEntityFeature @@ -74,11 +75,14 @@ class BaseSiren(PlatformEntity, ABC): _attr_supported_features: SirenEntityFeature @property - def state(self) -> dict[str, Any]: + def state(self) -> SirenState: """Get the state of the siren.""" - response = super().state - response["state"] = self.is_on - return response + return SirenState( + **super().state.__dict__, + is_on=self.is_on, + available_tones=self.available_tones, + supported_features=self.supported_features, + ) @property def is_on(self) -> bool: @@ -95,15 +99,6 @@ def supported_features(self) -> SirenEntityFeature: """Return supported features.""" return self._attr_supported_features - @functools.cached_property - def info_object(self) -> SirenEntityInfo: - """Return representation of the siren.""" - return SirenEntityInfo( - **super().info_object.__dict__, - available_tones=self.available_tones, - supported_features=self.supported_features, - ) - @abstractmethod async def async_turn_on( self, @@ -206,8 +201,8 @@ async def async_squawk( """Issue an IAS WD squawk command.""" squawk = Squawk() squawk.mode = mode - squawk.strobe = strobe - squawk.level = squawk_level + squawk.strobe = Strobe(strobe) # type:ignore[no-untyped-call] + squawk.level = SquawkLevel(squawk_level) # type:ignore[no-untyped-call] await self._cluster.squawk(squawk=squawk) def _async_set_off(self) -> None: diff --git a/zha/application/platforms/switch.py b/zha/application/platforms/switch.py index a6474a2ce..f5fc14212 100644 --- a/zha/application/platforms/switch.py +++ b/zha/application/platforms/switch.py @@ -4,9 +4,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from zigpy import types as t from zigpy.profiles import zha, zll @@ -28,7 +27,7 @@ from zha.application.platforms import ( AttrConfig, BaseEntity, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, EntityCategory, @@ -58,9 +57,17 @@ @dataclass(frozen=True, kw_only=True) -class ConfigurableAttributeSwitchInfo(BaseEntityInfo): - """Switch configuration entity info.""" +class SwitchState(BaseEntityState): + """State for switch entities.""" + is_on: bool + + +@dataclass(frozen=True, kw_only=True) +class ConfigurableAttributeSwitchState(SwitchState): + """State for configurable attribute switch entities.""" + + inverted: bool attribute_name: str invert_attribute_name: str | None force_inverted: bool @@ -74,11 +81,12 @@ class BaseSwitch(BaseEntity, ABC): PLATFORM = Platform.SWITCH @property - def state(self) -> dict[str, Any]: + def state(self) -> SwitchState: """Return the state of the switch.""" - response = super().state - response["state"] = self.is_on - return response + return SwitchState( + **super().state.__dict__, + is_on=self.is_on, + ) @property @abstractmethod @@ -95,7 +103,7 @@ async def async_turn_off(self) -> None: @register_entity(OnOff.cluster_id) -class Switch(PlatformEntity, BaseSwitch): +class Switch(PlatformEntity, BaseSwitch): # type: ignore[misc] """ZHA switch.""" _attr_translation_key = "switch" @@ -236,7 +244,7 @@ async def async_update(self) -> None: @register_entity(BinaryOutput.cluster_id) -class BinaryOutputSwitch(PlatformEntity, BaseSwitch): +class BinaryOutputSwitch(PlatformEntity, BaseSwitch): # type: ignore[misc] """BinaryOutputCluster switch.""" _attr_primary_weight = 10 @@ -335,7 +343,7 @@ async def async_update(self) -> None: @register_group_entity -class SwitchGroup(GroupEntity, BaseSwitch): +class SwitchGroup(GroupEntity, BaseSwitch): # type: ignore[misc] """Representation of a switch group.""" _attr_primary_weight = 10 @@ -345,8 +353,6 @@ def __init__(self, group: Group): super().__init__(group) self._state: bool self._cluster = group.zigpy_group.endpoint[OnOff.cluster_id] - if hasattr(self, "info_object"): - delattr(self, "info_object") self.update() @property @@ -374,11 +380,11 @@ def update(self, _: Any | None = None) -> None: """Query all members and determine the light group state.""" self.debug("Updating switch group entity state") platform_entities = self._group.get_platform_entities(self.PLATFORM) - all_states = [entity.state for entity in platform_entities] + all_states = [cast(SwitchState, entity.state) for entity in platform_entities] self.debug( "All platform entity states for group entity members: %s", all_states ) - on_states = [state for state in all_states if state["state"]] + on_states = [state for state in all_states if state.is_on] self._state = len(on_states) > 0 @@ -477,11 +483,13 @@ def _is_supported(self) -> bool: return super()._is_supported() - @functools.cached_property - def info_object(self) -> ConfigurableAttributeSwitchInfo: - """Return representation of the switch configuration entity.""" - return ConfigurableAttributeSwitchInfo( - **super().info_object.__dict__, + @property + def state(self) -> ConfigurableAttributeSwitchState: + """Return the state of the switch.""" + return ConfigurableAttributeSwitchState( + **super().state.__dict__, + is_on=self.is_on, + inverted=self.inverted, attribute_name=self._attribute_name, invert_attribute_name=self._inverter_attribute_name, force_inverted=self._force_inverted, @@ -489,14 +497,6 @@ def info_object(self) -> ConfigurableAttributeSwitchInfo: on_value=self._on_value, ) - @property - def state(self) -> dict[str, Any]: - """Return the state of the switch.""" - response = super().state - response["state"] = self.is_on - response["inverted"] = self.inverted - return response - @property def inverted(self) -> bool: """Return True if the switch is inverted.""" diff --git a/zha/application/platforms/update.py b/zha/application/platforms/update.py index cc7ce0a92..abe45f48b 100644 --- a/zha/application/platforms/update.py +++ b/zha/application/platforms/update.py @@ -5,7 +5,6 @@ from abc import ABC from dataclasses import dataclass from enum import IntFlag, StrEnum -import functools import itertools import logging from typing import TYPE_CHECKING, Any, Final @@ -24,7 +23,7 @@ from zha.application import Platform from zha.application.platforms import ( AttrConfig, - BaseEntityInfo, + BaseEntityState, ClusterConfig, ClusterMatch, EntityCategory, @@ -68,11 +67,17 @@ class UpdateEntityFeature(IntFlag): @dataclass(frozen=True, kw_only=True) -class UpdateEntityInfo(BaseEntityInfo): - """Update entity info.""" - +class UpdateState(BaseEntityState): + """State for update entities.""" + + installed_version: str | None + in_progress: bool | None + update_percentage: float | None + latest_version: str | None + release_summary: str | None + release_notes: str | None + release_url: str | None supported_features: UpdateEntityFeature - device_class: UpdateDeviceClass class BaseFirmwareUpdateEntity(PlatformEntity, ABC): @@ -95,29 +100,24 @@ class BaseFirmwareUpdateEntity(PlatformEntity, ABC): _attr_release_notes: str | None = None _attr_release_url: str | None = None - @functools.cached_property - def info_object(self) -> UpdateEntityInfo: - """Return a representation of the entity.""" - return UpdateEntityInfo( - **super().info_object.__dict__, - supported_features=self.supported_features, - ) - @property - def state(self) -> dict[str, Any]: + def state(self) -> UpdateState: """Get the state for the entity.""" - response = super().state - if (release_summary := self.release_summary) is not None: + release_summary = self.release_summary + if release_summary is not None: release_summary = release_summary[:255] - response[ATTR_INSTALLED_VERSION] = self.installed_version - response[ATTR_IN_PROGRESS] = self.in_progress - response[ATTR_UPDATE_PERCENTAGE] = self.update_percentage - response[ATTR_LATEST_VERSION] = self.latest_version - response[ATTR_RELEASE_SUMMARY] = release_summary - response[ATTR_RELEASE_NOTES] = self.release_notes - response[ATTR_RELEASE_URL] = self.release_url - return response + return UpdateState( + **super().state.__dict__, + installed_version=self.installed_version, + in_progress=self.in_progress, + update_percentage=self.update_percentage, + latest_version=self.latest_version, + release_summary=release_summary, + release_notes=self.release_notes, + release_url=self.release_url, + supported_features=self.supported_features, + ) @property def installed_version(self) -> str | None: @@ -308,6 +308,8 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity): ), } + _cluster: Ota + def __init__( self, endpoint: Endpoint, diff --git a/zha/event.py b/zha/event.py index 6a31f775b..daa15daae 100644 --- a/zha/event.py +++ b/zha/event.py @@ -3,7 +3,9 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Generator +import contextlib +from contextvars import ContextVar import dataclasses import inspect import logging @@ -11,6 +13,19 @@ _LOGGER = logging.getLogger(__package__) +_suppress_events: ContextVar[bool] = ContextVar("suppress_events", default=False) + + +@contextlib.contextmanager +def suppress_events() -> Generator[None, None, None]: + """Context manager to suppress event emission.""" + token = _suppress_events.set(True) + + try: + yield + finally: + _suppress_events.reset(token) + @dataclasses.dataclass(frozen=True, slots=True) class EventListener: @@ -86,6 +101,9 @@ def event_listener(*args, **kwargs) -> None: def emit(self, event_name: str, data=None) -> None: """Run all callbacks for an event.""" + if _suppress_events.get(): + return + listeners = [*self._listeners.get(event_name, []), *self._global_listeners] _LOGGER.debug( "Emitting event %s with data %r (%d listeners)", diff --git a/zha/zigbee/device.py b/zha/zigbee/device.py index d9a6a1cb0..364fb93c7 100644 --- a/zha/zigbee/device.py +++ b/zha/zigbee/device.py @@ -77,14 +77,14 @@ from zha.application.helpers import convert_to_zcl_values, convert_zcl_value, safe_read from zha.application.platforms import ( BaseEntity, - BaseEntityInfo, + BaseEntityState, EntityStateChangedEvent, PlatformEntity, sensor, ) from zha.application.platforms.update import BaseFirmwareUpdateEntity from zha.const import STATE_CHANGED -from zha.event import EventBase +from zha.event import EventBase, suppress_events from zha.exceptions import ZHAException from zha.mixins import LogMixin from zha.quirks import ( @@ -323,7 +323,7 @@ class ExtendedDeviceInfo(DeviceInfo): """Describes a ZHA device.""" active_coordinator: bool - entities: dict[str, BaseEntityInfo] + entities: dict[str, BaseEntityState] neighbors: list[NeighborInfo] routes: list[RouteInfo] endpoint_names: list[EndpointNameInfo] @@ -963,7 +963,7 @@ def extended_device_info(self) -> ExtendedDeviceInfo: **self.device_info.__dict__, active_coordinator=self.is_active_coordinator, entities={ - platform_entity.unique_id: platform_entity.info_object + platform_entity.unique_id: platform_entity.state for platform_entity in self.platform_entities.values() }, neighbors=[ @@ -1185,6 +1185,19 @@ async def _add_pending_entities(self, *, emit_event: bool = True) -> None: for entity in new_entities.values(): self._add_entity(entity, emit_event=emit_event) + # New entities have no listener yet (consumers capture their initial state when + # the add event registers them), so silence their changes + with suppress_events(): + for entity in new_entities.values(): + entity.maybe_emit_state_changed_event() + + # `_compute_primary_entity` above can flip `primary` on an existing entity, and + # the caller may have recomputed their capabilities beforehand; emit so those + # changes reach consumers. + for key, entity in all_entities.items(): + if key not in new_entities: + entity.maybe_emit_state_changed_event() + async def recompute_entities(self) -> None: """Recompute all entities for this device.""" self.debug("Recomputing entities") @@ -1614,11 +1627,7 @@ def _compute_primary_entity(self, entities: Sequence[PlatformEntity]) -> None: # For weight matching, only consider non-counter entities and entities which are # not explicitly marked as not primary - candidates = [ - e - for e in entities - if e.enabled and hasattr(e, "info_object") and e._attr_primary is not False - ] + candidates = [e for e in entities if e.enabled and e._attr_primary is not False] candidates.sort(reverse=True, key=lambda e: e.primary_weight) if not candidates: @@ -1630,11 +1639,9 @@ def _compute_primary_entity(self, entities: Sequence[PlatformEntity]) -> None: # We have a clear winner if not others or winner.primary_weight > others[0].primary_weight: winner.primary = True - del winner.info_object for entity in others: entity.primary = False - del entity.info_object return @@ -1644,7 +1651,6 @@ def _compute_primary_entity(self, entities: Sequence[PlatformEntity]) -> None: for entity in candidates: entity.primary = False - del entity.info_object def get_diagnostics_json(self): """Get ZHA device information.""" @@ -1768,21 +1774,14 @@ def get_diagnostics_json(self): if platform is Platform.VIRTUAL: continue - info_object = dataclasses.asdict(platform_entity.info_object) - info_object["migrate_unique_ids"] = list(info_object["migrate_unique_ids"]) - info_object["device_ieee"] = str(info_object["device_ieee"]) - - obj: dict[str, Any] = { - "info_object": info_object, - "state": platform_entity.state, - } - - if platform_entity.extra_state_attribute_names is not None: - obj["extra_state_attributes"] = sorted( - platform_entity.extra_state_attribute_names - ) + state_dict = dataclasses.asdict(platform_entity.state) + state_dict["migrate_unique_ids"] = list(state_dict["migrate_unique_ids"]) + state_dict["device_ieee"] = str(state_dict["device_ieee"]) + state_dict["extra_state_attribute_names"] = sorted( + state_dict["extra_state_attribute_names"] + ) - info["zha_lib_entities"][platform].append(obj) + info["zha_lib_entities"][platform].append(state_dict) topology = self.gateway.application_controller.topology info["neighbors"] = [ diff --git a/zha/zigbee/group.py b/zha/zigbee/group.py index 9f16b40af..ab09301f5 100644 --- a/zha/zigbee/group.py +++ b/zha/zigbee/group.py @@ -13,7 +13,7 @@ from zigpy.types.named import EUI64 from zha.application.platforms import ( - BaseEntityInfo, + BaseEntityState, EntityStateChangedEvent, PlatformEntity, ) @@ -54,7 +54,7 @@ class GroupMemberInfo: ieee: EUI64 endpoint_id: int device_info: ExtendedDeviceInfo - entities: dict[str, BaseEntityInfo] + entities: dict[str, BaseEntityState] @dataclass(frozen=True, kw_only=True) @@ -64,7 +64,7 @@ class GroupInfo: group_id: int name: str members: list[GroupMemberInfo] - entities: dict[str, BaseEntityInfo] + entities: dict[str, BaseEntityState] class GroupMember(LogMixin): @@ -104,8 +104,7 @@ def member_info(self) -> GroupMemberInfo: endpoint_id=self.endpoint_id, device_info=self.device.extended_device_info, entities={ - entity.unique_id: entity.info_object - for entity in self.associated_entities + entity.unique_id: entity.state for entity in self.associated_entities }, ) @@ -198,14 +197,14 @@ def members(self) -> list[GroupMember]: ] @cached_property - def info_object(self) -> GroupInfo: + def state(self) -> GroupInfo: """Get ZHA group info.""" return GroupInfo( group_id=self.group_id, name=self.name, members=[member.member_info for member in self.members], entities={ - unique_id: entity.info_object + unique_id: entity.state for unique_id, entity in self._group_entities.items() }, ) @@ -254,8 +253,8 @@ def clear_caches(self) -> None: """Clear cached properties.""" if hasattr(self, "all_member_entity_unique_ids"): delattr(self, "all_member_entity_unique_ids") - if hasattr(self, "info_object"): - delattr(self, "info_object") + if hasattr(self, "state"): + delattr(self, "state") if hasattr(self, "members"): delattr(self, "members")