From db12f68ff061b5a67b299761d796109a2127f603 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 30 Mar 2024 17:10:59 -0400 Subject: [PATCH] Clean up events and properties (#20) * to_json -> info_object and dataclasses * Make state a property * cached properties --- pyproject.toml | 6 + tests/test_alarm_control_panel.py | 39 +-- tests/test_binary_sensor.py | 4 +- tests/test_climate.py | 233 ++++++++------- tests/test_cover.py | 80 +++--- tests/test_debouncer.py | 14 +- tests/test_device.py | 4 +- tests/test_device_tracker.py | 12 +- tests/test_discover.py | 6 +- tests/test_fan.py | 125 ++++---- tests/test_gateway.py | 70 ++--- tests/test_light.py | 226 +++++++-------- tests/test_lock.py | 18 +- tests/test_number.py | 42 +-- tests/test_select.py | 18 +- tests/test_sensor.py | 72 ++--- tests/test_siren.py | 14 +- tests/test_switch.py | 66 ++--- tests/test_update.py | 75 +++-- zha/application/gateway.py | 186 ++++++++---- zha/application/platforms/__init__.py | 198 ++++++++----- .../platforms/alarm_control_panel/__init__.py | 93 +++--- .../platforms/binary_sensor/__init__.py | 54 ++-- zha/application/platforms/button/__init__.py | 55 +++- zha/application/platforms/climate/__init__.py | 82 +++--- zha/application/platforms/cover/__init__.py | 141 ++++----- zha/application/platforms/device_tracker.py | 71 ++--- zha/application/platforms/fan/__init__.py | 152 ++++++---- zha/application/platforms/light/__init__.py | 150 ++++++---- zha/application/platforms/lock/__init__.py | 15 +- zha/application/platforms/number/__init__.py | 111 ++++--- zha/application/platforms/select.py | 69 +++-- zha/application/platforms/sensor/__init__.py | 200 ++++++++----- zha/application/platforms/siren.py | 42 ++- zha/application/platforms/switch.py | 87 ++++-- zha/application/platforms/update.py | 48 ++-- zha/zigbee/cluster_handlers/__init__.py | 75 +++-- zha/zigbee/device.py | 271 +++++++++++------- zha/zigbee/endpoint.py | 8 +- zha/zigbee/group.py | 149 ++++++---- 40 files changed, 1961 insertions(+), 1420 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a30eca5..63a375b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,12 @@ disable_error_code = [ "var-annotated", ] +[[tool.mypy.overrides]] +module = [ + "tests.*", +] +warn_unreachable = false + [tool.pylint] max-line-length = 120 disable = ["C0103", "W0212"] diff --git a/tests/test_alarm_control_panel.py b/tests/test_alarm_control_panel.py index 9e733f6..0630da5 100644 --- a/tests/test_alarm_control_panel.py +++ b/tests/test_alarm_control_panel.py @@ -12,6 +12,7 @@ from zha.application.gateway import Gateway from zha.application.platforms.alarm_control_panel import AlarmControlPanel +from zha.application.platforms.alarm_control_panel.const import AlarmState from zha.zigbee.device import Device from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -54,7 +55,7 @@ async def test_alarm_control_panel( assert isinstance(alarm_entity, AlarmControlPanel) # test that the state is STATE_ALARM_DISARMED - assert alarm_entity.state == "disarmed" + assert alarm_entity.state["state"] == AlarmState.DISARMED # arm_away cluster.client_command.reset_mock() @@ -69,7 +70,7 @@ async def test_alarm_control_panel( security.IasAce.AudibleNotification.Default_Sound, security.IasAce.AlarmStatus.No_Alarm, ) - assert alarm_entity.state == "armed_away" + assert alarm_entity.state["state"] == AlarmState.ARMED_AWAY # disarm await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -78,7 +79,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 == "armed_away" + assert alarm_entity.state["state"] == AlarmState.ARMED_AWAY cluster.client_command.reset_mock() # now simulate a faulty code entry sequence @@ -87,7 +88,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 == "triggered" + assert alarm_entity.state["state"] == AlarmState.TRIGGERED assert cluster.client_command.call_count == 6 assert cluster.client_command.await_count == 6 assert cluster.client_command.call_args == call( @@ -104,7 +105,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 == "armed_home" + assert alarm_entity.state["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( @@ -121,7 +122,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 == "armed_night" + assert alarm_entity.state["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( @@ -140,7 +141,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 == "armed_away" + assert alarm_entity.state["state"] == AlarmState.ARMED_AWAY # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -150,7 +151,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 == "armed_home" + assert alarm_entity.state["state"] == AlarmState.ARMED_HOME # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -160,33 +161,33 @@ 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 == "armed_night" + assert alarm_entity.state["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 == "armed_night" + assert alarm_entity.state["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 == "triggered" + assert alarm_entity.state["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 == "disarmed" + assert alarm_entity.state["state"] == AlarmState.DISARMED # panic from panel cluster.listener_event("cluster_command", 1, 4, []) await zha_gateway.async_block_till_done() - assert alarm_entity.state == "triggered" + assert alarm_entity.state["state"] == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -194,7 +195,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 == "triggered" + assert alarm_entity.state["state"] == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) @@ -202,19 +203,19 @@ 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 == "triggered" + assert alarm_entity.state["state"] == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) - assert alarm_entity.state == "disarmed" + assert alarm_entity.state["state"] == AlarmState.DISARMED await alarm_entity.async_alarm_trigger() await zha_gateway.async_block_till_done() - assert alarm_entity.state == "triggered" + assert alarm_entity.state["state"] == AlarmState.TRIGGERED # reset the panel await reset_alarm_panel(zha_gateway, cluster, alarm_entity) - assert alarm_entity.state == "disarmed" + assert alarm_entity.state["state"] == AlarmState.DISARMED async def reset_alarm_panel( @@ -226,7 +227,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 == "disarmed" + assert entity.state["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_binary_sensor.py b/tests/test_binary_sensor.py index d2dc2a4..6726114 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -53,7 +53,7 @@ async def async_test_binary_sensor_occupancy( assert entity.is_on is False # test refresh - cluster.read_attributes.reset_mock() # type: ignore[unreachable] + cluster.read_attributes.reset_mock() assert entity.is_on is False cluster.PLUGGED_ATTR_READS = plugs update_attribute_cache(cluster) @@ -84,7 +84,7 @@ async def async_test_iaszone_on_off( assert entity.is_on is False # check that binary sensor remains off when non-alarm bits change - cluster.listener_event("cluster_command", 1, 0, [0b1111111100]) # type: ignore[unreachable] + cluster.listener_event("cluster_command", 1, 0, [0b1111111100]) await zha_gateway.async_block_till_done() assert entity.is_on is False diff --git a/tests/test_climate.py b/tests/test_climate.py index a4d0612..38e3b21 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -315,10 +315,10 @@ async def test_climate_local_temperature( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_state()["current_temperature"] is None + assert entity.state["current_temperature"] is None await send_attributes_report(zha_gateway, thrm_cluster, {0: 2100}) - assert entity.get_state()["current_temperature"] == 21.0 + assert entity.state["current_temperature"] == 21.0 async def test_climate_hvac_action_running_state( @@ -341,50 +341,49 @@ async def test_climate_hvac_action_running_state( assert sensor_entity is not None assert isinstance(sensor_entity, SinopeHVACAction) - assert entity.get_state()["hvac_action"] == "off" - assert sensor_entity.get_state()["state"] == "off" + assert entity.state["hvac_action"] == "off" + assert sensor_entity.state["state"] == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) - assert entity.get_state()["hvac_action"] == "off" - assert sensor_entity.get_state()["state"] == "off" + assert entity.state["hvac_action"] == "off" + assert sensor_entity.state["state"] == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto} ) - assert entity.get_state()["hvac_action"] == "idle" - assert sensor_entity.get_state()["state"] == "idle" + assert entity.state["hvac_action"] == "idle" + assert sensor_entity.state["state"] == "idle" await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool} ) - assert entity.get_state()["hvac_action"] == "cooling" - assert sensor_entity.get_state()["state"] == "cooling" + assert entity.state["hvac_action"] == "cooling" + assert sensor_entity.state["state"] == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat} ) - assert entity.get_state()["hvac_action"] == "heating" - assert sensor_entity.get_state()["state"] == "heating" + assert entity.state["hvac_action"] == "heating" + assert sensor_entity.state["state"] == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) - assert entity.get_state()["hvac_action"] == "idle" - assert sensor_entity.get_state()["state"] == "idle" + assert entity.state["hvac_action"] == "idle" + assert sensor_entity.state["state"] == "idle" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) - assert entity.get_state()["hvac_action"] == "fan" - assert sensor_entity.get_state()["state"] == "fan" + assert entity.state["hvac_action"] == "fan" + assert sensor_entity.state["state"] == "fan" @pytest.mark.looptime async def test_sinope_time( device_climate_sinope: Device, - zha_gateway: Gateway, ): """Test hvac action via running state.""" @@ -425,62 +424,62 @@ async def test_climate_hvac_action_running_state_zen( assert sensor_entity is not None assert isinstance(sensor_entity, ThermostatHVACAction) - assert entity.get_state()["hvac_action"] is None - assert sensor_entity.get_state()["state"] is None + assert entity.state["hvac_action"] is None + assert sensor_entity.state["state"] is None await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} ) - assert entity.get_state()["hvac_action"] == "cooling" - assert sensor_entity.get_state()["state"] == "cooling" + assert entity.state["hvac_action"] == "cooling" + assert sensor_entity.state["state"] == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) - assert entity.get_state()["hvac_action"] == "fan" - assert sensor_entity.get_state()["state"] == "fan" + assert entity.state["hvac_action"] == "fan" + assert sensor_entity.state["state"] == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} ) - assert entity.get_state()["hvac_action"] == "heating" - assert sensor_entity.get_state()["state"] == "heating" + assert entity.state["hvac_action"] == "heating" + assert sensor_entity.state["state"] == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} ) - assert entity.get_state()["hvac_action"] == "fan" - assert sensor_entity.get_state()["state"] == "fan" + assert entity.state["hvac_action"] == "fan" + assert sensor_entity.state["state"] == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} ) - assert entity.get_state()["hvac_action"] == "cooling" - assert sensor_entity.get_state()["state"] == "cooling" + assert entity.state["hvac_action"] == "cooling" + assert sensor_entity.state["state"] == "cooling" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} ) - assert entity.get_state()["hvac_action"] == "fan" - assert sensor_entity.get_state()["state"] == "fan" + assert entity.state["hvac_action"] == "fan" + assert sensor_entity.state["state"] == "fan" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} ) - assert entity.get_state()["hvac_action"] == "heating" - assert sensor_entity.get_state()["state"] == "heating" + assert entity.state["hvac_action"] == "heating" + assert sensor_entity.state["state"] == "heating" await send_attributes_report( zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} ) - assert entity.get_state()["hvac_action"] == "off" - assert sensor_entity.get_state()["state"] == "off" + assert entity.state["hvac_action"] == "off" + assert sensor_entity.state["state"] == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) - assert entity.get_state()["hvac_action"] == "idle" - assert sensor_entity.get_state()["state"] == "idle" + assert entity.state["hvac_action"] == "idle" + assert sensor_entity.state["state"] == "idle" async def test_climate_hvac_action_pi_demand( @@ -497,28 +496,28 @@ async def test_climate_hvac_action_pi_demand( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_state()["hvac_action"] is None + assert entity.state["hvac_action"] is None await send_attributes_report(zha_gateway, thrm_cluster, {0x0007: 10}) - assert entity.get_state()["hvac_action"] == "cooling" + assert entity.state["hvac_action"] == "cooling" await send_attributes_report(zha_gateway, thrm_cluster, {0x0008: 20}) - assert entity.get_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.get_state()["hvac_action"] == "off" + assert entity.state["hvac_action"] == "off" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) - assert entity.get_state()["hvac_action"] == "idle" + assert entity.state["hvac_action"] == "idle" await send_attributes_report( zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Cool} ) - assert entity.get_state()["hvac_action"] == "idle" + assert entity.state["hvac_action"] == "idle" @pytest.mark.parametrize( @@ -548,18 +547,18 @@ async def test_hvac_mode( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_state()["hvac_mode"] == "off" + assert entity.state["hvac_mode"] == "off" await send_attributes_report(zha_gateway, thrm_cluster, {0x001C: sys_mode}) - assert entity.get_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.get_state()["hvac_mode"] == "off" + assert entity.state["hvac_mode"] == "off" await send_attributes_report(zha_gateway, thrm_cluster, {0x001C: 0xFF}) - assert entity.get_state()["hvac_mode"] is None + assert entity.state["hvac_mode"] is None @pytest.mark.parametrize( @@ -632,7 +631,7 @@ async def test_target_temperature( await entity.async_set_preset_mode(preset) await zha_gateway.async_block_till_done() - assert entity.get_state()["target_temperature"] == target_temp + assert entity.state["target_temperature"] == target_temp @pytest.mark.parametrize( @@ -671,7 +670,7 @@ async def test_target_temperature_high( await entity.async_set_preset_mode(preset) await zha_gateway.async_block_till_done() - assert entity.get_state()["target_temperature_high"] == target_temp + assert entity.state["target_temperature_high"] == target_temp @pytest.mark.parametrize( @@ -710,7 +709,7 @@ async def test_target_temperature_low( await entity.async_set_preset_mode(preset) await zha_gateway.async_block_till_done() - assert entity.get_state()["target_temperature_low"] == target_temp + assert entity.state["target_temperature_low"] == target_temp @pytest.mark.parametrize( @@ -739,27 +738,27 @@ async def test_set_hvac_mode( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_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.get_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.get_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 @@ -779,7 +778,7 @@ async def test_preset_setting( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_state()["preset_mode"] == "none" + assert entity.state["preset_mode"] == "none" # unsuccessful occupancy change thrm_cluster.write_attributes.return_value = [ @@ -797,7 +796,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() - assert entity.get_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} @@ -809,7 +808,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() - assert entity.get_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} @@ -831,7 +830,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("none") await zha_gateway.async_block_till_done() - assert entity.get_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} @@ -844,7 +843,7 @@ async def test_preset_setting( await entity.async_set_preset_mode("none") await zha_gateway.async_block_till_done() - assert entity.get_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} @@ -863,11 +862,11 @@ async def test_preset_setting_invalid( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_state()["preset_mode"] == "none" + assert entity.state["preset_mode"] == "none" assert thrm_cluster.write_attributes.call_count == 0 @@ -885,11 +884,11 @@ async def test_set_temperature_hvac_mode( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_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 @@ -921,20 +920,20 @@ async def test_set_temperature_heat_cool( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_state()["target_temperature_low"] == 20.0 - assert entity.get_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.get_state()["target_temperature_low"] == 19.0 - assert entity.get_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 @@ -950,8 +949,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.get_state()["target_temperature_low"] == 15.0 - assert entity.get_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 @@ -986,22 +985,22 @@ async def test_set_temperature_heat( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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 @@ -1014,9 +1013,9 @@ async def test_set_temperature_heat( await entity.async_set_temperature(temperature=22) await zha_gateway.async_block_till_done() - assert entity.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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 @@ -1048,22 +1047,22 @@ async def test_set_temperature_cool( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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 @@ -1076,9 +1075,9 @@ async def test_set_temperature_cool( await entity.async_set_temperature(temperature=22) await zha_gateway.async_block_till_done() - assert entity.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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 @@ -1114,14 +1113,14 @@ async def test_set_temperature_wrong_mode( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_state()["target_temperature_low"] is None - assert entity.get_state()["target_temperature_high"] is None - assert entity.get_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 @@ -1138,20 +1137,20 @@ async def test_occupancy_reset( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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.get_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.get_state()["preset_mode"] == "none" + assert entity.state["preset_mode"] == "none" async def test_fan_mode( @@ -1168,26 +1167,26 @@ async def test_fan_mode( assert isinstance(entity, ThermostatEntity) assert set(entity.fan_modes) == {FanState.AUTO, FanState.ON} - assert entity.get_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.get_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.get_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.get_state()["fan_mode"] == FanState.ON + assert entity.state["fan_mode"] == FanState.ON async def test_set_fan_mode_not_supported( @@ -1221,7 +1220,7 @@ async def test_set_fan_mode( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_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() @@ -1246,7 +1245,7 @@ async def test_set_moes_preset(device_climate_moes: Device, zha_gateway: Gateway assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_state()["preset_mode"] == "none" + assert entity.state["preset_mode"] == "none" await entity.async_set_preset_mode("away") await zha_gateway.async_block_till_done() @@ -1340,31 +1339,31 @@ async def test_set_moes_operation_mode( await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 0}) - assert entity.get_state()["preset_mode"] == "away" + assert entity.state["preset_mode"] == "away" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 1}) - assert entity.get_state()["preset_mode"] == "Schedule" + assert entity.state["preset_mode"] == "Schedule" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 2}) - assert entity.get_state()["preset_mode"] == "none" + assert entity.state["preset_mode"] == "none" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 3}) - assert entity.get_state()["preset_mode"] == "comfort" + assert entity.state["preset_mode"] == "comfort" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 4}) - assert entity.get_state()["preset_mode"] == "eco" + assert entity.state["preset_mode"] == "eco" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 5}) - assert entity.get_state()["preset_mode"] == "boost" + assert entity.state["preset_mode"] == "boost" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 6}) - assert entity.get_state()["preset_mode"] == "Complex" + assert entity.state["preset_mode"] == "Complex" # Device is running an energy-saving mode @@ -1401,7 +1400,7 @@ async def test_beca_operation_mode_update( zha_gateway, thrm_cluster, {"operation_preset": preset_attr} ) - assert entity.get_state()[ATTR_PRESET_MODE] == preset_mode + assert entity.state[ATTR_PRESET_MODE] == preset_mode await entity.async_set_preset_mode(preset_mode) await zha_gateway.async_block_till_done() @@ -1425,7 +1424,7 @@ async def test_set_zonnsmart_preset( assert entity is not None assert isinstance(entity, ThermostatEntity) - assert entity.get_state()[ATTR_PRESET_MODE] == PRESET_NONE + assert entity.state[ATTR_PRESET_MODE] == PRESET_NONE await entity.async_set_preset_mode(PRESET_SCHEDULE) await zha_gateway.async_block_till_done() @@ -1483,20 +1482,20 @@ async def test_set_zonnsmart_operation_mode( await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 0}) - assert entity.get_state()[ATTR_PRESET_MODE] == PRESET_SCHEDULE + assert entity.state[ATTR_PRESET_MODE] == PRESET_SCHEDULE await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 1}) - assert entity.get_state()[ATTR_PRESET_MODE] == PRESET_NONE + assert entity.state[ATTR_PRESET_MODE] == PRESET_NONE await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 2}) - assert entity.get_state()[ATTR_PRESET_MODE] == "holiday" + assert entity.state[ATTR_PRESET_MODE] == "holiday" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 3}) - assert entity.get_state()[ATTR_PRESET_MODE] == "holiday" + assert entity.state[ATTR_PRESET_MODE] == "holiday" await send_attributes_report(zha_gateway, thrm_cluster, {"operation_preset": 4}) - assert entity.get_state()[ATTR_PRESET_MODE] == "frost protect" + assert entity.state[ATTR_PRESET_MODE] == "frost protect" diff --git a/tests/test_cover.py b/tests/test_cover.py index 7e0da05..31445cd 100644 --- a/tests/test_cover.py +++ b/tests/test_cover.py @@ -162,7 +162,7 @@ async def test_cover_non_tilt_initial_state( # pylint: disable=unused-argument entity = get_entity(zha_device, entity_id) assert entity is not None - state = entity.get_state() + state = entity.state assert state["state"] == STATE_OPEN assert state[ATTR_CURRENT_POSITION] == 100 @@ -177,8 +177,8 @@ async def test_cover_non_tilt_initial_state( # pylint: disable=unused-argument await entity.async_update() assert cluster.read_attributes.call_count == prev_call_count + 1 - assert entity.get_state()["state"] == STATE_CLOSED - assert entity.get_state()[ATTR_CURRENT_POSITION] == 0 + assert entity.state["state"] == STATE_CLOSED + assert entity.state[ATTR_CURRENT_POSITION] == 0 async def test_cover( @@ -225,31 +225,31 @@ async def test_cover( await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 100} ) - assert entity.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # test to see if it opens await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 0} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # test that the state remains after tilting to 100% await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 100} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # 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 entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN cluster.PLUGGED_ATTR_READS = {1: 100} update_attribute_cache(cluster) await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # close from client with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): @@ -261,13 +261,13 @@ 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.get_state()["state"] == STATE_CLOSING + assert entity.state["state"] == STATE_CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 100} ) - assert entity.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # tilt close from client with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): @@ -283,13 +283,13 @@ async def test_cover( assert cluster.request.call_args[0][3] == 100 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.get_state()["state"] == STATE_CLOSING + assert entity.state["state"] == STATE_CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 100} ) - assert entity.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # open from client with patch("zigpy.zcl.Cluster.request", return_value=[0x0, zcl_f.Status.SUCCESS]): @@ -301,13 +301,13 @@ 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.get_state()["state"] == STATE_OPENING + assert entity.state["state"] == STATE_OPENING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 0} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # open tilt from client with patch("zigpy.zcl.Cluster.request", return_value=[0x0, zcl_f.Status.SUCCESS]): @@ -323,13 +323,13 @@ async def test_cover( assert cluster.request.call_args[0][3] == 0 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.get_state()["state"] == STATE_OPENING + assert entity.state["state"] == STATE_OPENING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_tilt_percentage.id: 0} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # set position UI with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): @@ -342,19 +342,19 @@ async def test_cover( assert cluster.request.call_args[0][3] == 53 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.get_state()["state"] == STATE_CLOSING + assert entity.state["state"] == STATE_CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 35} ) - assert entity.get_state()["state"] == STATE_CLOSING + assert entity.state["state"] == STATE_CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 53} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # set tilt position UI with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): @@ -370,19 +370,19 @@ async def test_cover( assert cluster.request.call_args[0][3] == 53 assert cluster.request.call_args[1]["expect_reply"] is True - assert entity.get_state()["state"] == STATE_CLOSING + assert entity.state["state"] == STATE_CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 35} ) - assert entity.get_state()["state"] == STATE_CLOSING + assert entity.state["state"] == STATE_CLOSING await send_attributes_report( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 53} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # stop from client with patch("zigpy.zcl.Cluster.request", return_value=[0x2, zcl_f.Status.SUCCESS]): @@ -429,7 +429,7 @@ async def test_cover_failures( zha_gateway, cluster, {WCAttrs.current_position_lift_percentage.id: 0} ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # close from UI with patch( @@ -447,7 +447,7 @@ async def test_cover_failures( cluster.request.call_args[0][1] == closures.WindowCovering.ServerCommandDefs.down_close.id ) - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN with patch( "zigpy.zcl.Cluster.request", @@ -587,17 +587,17 @@ async def test_shade( await send_attributes_report( zha_gateway, cluster_on_off, {cluster_on_off.AttributeDefs.on_off.id: 0} ) - assert entity.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_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.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # close from client command fails with ( @@ -615,7 +615,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.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN with patch( "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x1, zcl_f.Status.SUCCESS]) @@ -625,11 +625,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.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # open from client command fails await send_attributes_report(zha_gateway, cluster_level, {0: 0}) - assert entity.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED with ( patch( @@ -646,7 +646,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.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # open from client succeeds with patch( @@ -657,7 +657,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.get_state()["state"] == STATE_OPEN + assert entity.state["state"] == STATE_OPEN # set position UI command fails with ( @@ -676,7 +676,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.get_state()["current_position"] == 0 + assert entity.state["current_position"] == 0 # set position UI success with patch( @@ -688,11 +688,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.get_state()["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.get_state()["current_position"] == int(100 * 100 / 255) + assert entity.state["current_position"] == int(100 * 100 / 255) # stop command fails with ( @@ -740,11 +740,11 @@ 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.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # open from client command fails p1 = patch.object(cluster_on_off, "request", side_effect=asyncio.TimeoutError) @@ -760,7 +760,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.get_state()["state"] == STATE_CLOSED + assert entity.state["state"] == STATE_CLOSED # open from client command success p1 = patch.object(cluster_on_off, "request", AsyncMock(return_value=[1, 0])) @@ -773,8 +773,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.get_state()["state"] == STATE_OPEN - assert entity.get_state()["current_position"] == 100 + assert entity.state["state"] == STATE_OPEN + assert entity.state["current_position"] == 100 async def test_cover_remote( diff --git a/tests/test_debouncer.py b/tests/test_debouncer.py index f65aa7f..c0e8bbc 100644 --- a/tests/test_debouncer.py +++ b/tests/test_debouncer.py @@ -37,7 +37,7 @@ async def test_immediate_works(zha_gateway: Gateway) -> None: assert len(calls) == 1 assert debouncer._timer_task is not None assert debouncer._execute_at_end_of_timer is True - assert debouncer._job.target == debouncer.function # type: ignore[unreachable] + assert debouncer._job.target == debouncer.function # Canceling debounce in cooldown debouncer.async_cancel() @@ -94,7 +94,7 @@ async def test_immediate_works_with_schedule_call(zha_gateway: Gateway) -> None: assert len(calls) == 1 assert debouncer._timer_task is not None assert debouncer._execute_at_end_of_timer is True - assert debouncer._job.target == debouncer.function # type: ignore[unreachable] + assert debouncer._job.target == debouncer.function # Canceling debounce in cooldown debouncer.async_cancel() @@ -202,7 +202,7 @@ def _append_and_raise() -> None: assert len(calls) == 1 assert debouncer._timer_task is not None assert debouncer._execute_at_end_of_timer is True - assert debouncer._job.target == debouncer.function # type: ignore[unreachable] + assert debouncer._job.target == debouncer.function # Canceling debounce in cooldown debouncer.async_cancel() @@ -266,7 +266,7 @@ async def _append_and_raise() -> None: assert len(calls) == 1 assert debouncer._timer_task is not None assert debouncer._execute_at_end_of_timer is True - assert debouncer._job.target == debouncer.function # type: ignore[unreachable] + assert debouncer._job.target == debouncer.function # Canceling debounce in cooldown debouncer.async_cancel() @@ -325,7 +325,7 @@ async def test_not_immediate_works(zha_gateway: Gateway) -> None: # Canceling while on cooldown debouncer.async_cancel() assert debouncer._timer_task is None - assert debouncer._execute_at_end_of_timer is False # type: ignore[unreachable] + assert debouncer._execute_at_end_of_timer is False # Call and let timer run out await debouncer.async_call() @@ -379,7 +379,7 @@ async def test_not_immediate_works_schedule_call(zha_gateway: Gateway) -> None: # Canceling while on cooldown debouncer.async_cancel() assert debouncer._timer_task is None - assert debouncer._execute_at_end_of_timer is False # type: ignore[unreachable] + assert debouncer._execute_at_end_of_timer is False # Call and let timer run out debouncer.async_schedule_call() @@ -434,7 +434,7 @@ async def test_immediate_works_with_function_swapped(zha_gateway: Gateway) -> No assert len(calls) == 1 assert debouncer._timer_task is not None assert debouncer._execute_at_end_of_timer is True - assert debouncer._job.target == debouncer.function # type: ignore[unreachable] + assert debouncer._job.target == debouncer.function # Canceling debounce in cooldown debouncer.async_cancel() diff --git a/tests/test_device.py b/tests/test_device.py index 1dd2d6c..f966662 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -142,7 +142,7 @@ async def test_check_available_success( assert zha_device.available is True await _send_time_changed(zha_gateway, zha_device.consider_unavailable_time + 2) assert zha_device.available is False - assert basic_ch.read_attributes.await_count == 0 # type: ignore[unreachable] + assert basic_ch.read_attributes.await_count == 0 device_with_basic_cluster_handler.last_seen = ( time.time() - zha_device.consider_unavailable_time - 100 @@ -244,7 +244,7 @@ async def test_check_available_no_basic_cluster_handler( await _send_time_changed(zha_gateway, zha_device.__polling_interval + 1) assert zha_device.available is False - assert "does not have a mandatory basic cluster" in caplog.text # type: ignore[unreachable] + assert "does not have a mandatory basic cluster" in caplog.text async def test_device_is_active_coordinator( diff --git a/tests/test_device_tracker.py b/tests/test_device_tracker.py index fecfc83..cde1fa9 100644 --- a/tests/test_device_tracker.py +++ b/tests/test_device_tracker.py @@ -63,7 +63,7 @@ async def test_device_tracker( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["connected"] is False + assert entity.state["connected"] is False # turn state flip await send_attributes_report( @@ -76,7 +76,7 @@ async def test_device_tracker( await zha_gateway.async_block_till_done() assert entity.async_update.await_count == 1 - assert entity.get_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 @@ -85,19 +85,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.get_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() # type: ignore[unreachable] + zigpy_device_dt.last_seen = time.time() await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_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.get_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 fa1def0..131e05c 100644 --- a/tests/test_discover.py +++ b/tests/test_discover.py @@ -678,7 +678,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): power_source_entity = get_entity(zha_device, power_source_entity_id) assert power_source_entity is not None assert ( - power_source_entity.get_state()["state"] + power_source_entity.state["state"] == BasicCluster.PowerSource.Mains_single_phase.name ) @@ -690,7 +690,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): assert hook_state_entity_id is not None hook_state_entity = get_entity(zha_device, hook_state_entity_id) assert hook_state_entity is not None - assert hook_state_entity.get_state()["state"] == AqaraE1HookState.Unlocked.name + assert hook_state_entity.state["state"] == AqaraE1HookState.Unlocked.name error_detected_entity_id = find_entity_id( Platform.BINARY_SENSOR, @@ -699,7 +699,7 @@ class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): assert error_detected_entity_id is not None error_detected_entity = get_entity(zha_device, error_detected_entity_id) assert error_detected_entity is not None - assert error_detected_entity.get_state()["state"] is False + assert error_detected_entity.state["state"] is False def _get_test_device( diff --git a/tests/test_fan.py b/tests/test_fan.py index 0270f9c..269c3e3 100644 --- a/tests/test_fan.py +++ b/tests/test_fan.py @@ -21,7 +21,6 @@ from zha.application.platforms import GroupEntity, PlatformEntity from zha.application.platforms.fan.const import ( ATTR_PERCENTAGE, - ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, PRESET_MODE_AUTO, PRESET_MODE_ON, @@ -151,15 +150,15 @@ async def test_fan( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_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.get_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.get_state()["is_on"] is False + assert entity.state["is_on"] is False # turn on from client cluster.write_attributes.reset_mock() @@ -168,7 +167,7 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 2}, manufacturer=None ) - assert entity.get_state()["is_on"] is True + assert entity.state["is_on"] is True # turn off from client cluster.write_attributes.reset_mock() @@ -177,7 +176,7 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 0}, manufacturer=None ) - assert entity.get_state()["is_on"] is False + assert entity.state["is_on"] is False # change speed from client cluster.write_attributes.reset_mock() @@ -186,8 +185,8 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 3}, manufacturer=None ) - assert entity.get_state()["is_on"] is True - assert entity.get_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() @@ -196,8 +195,8 @@ async def test_fan( assert cluster.write_attributes.call_args == call( {"fan_mode": 4}, manufacturer=None ) - assert entity.get_state()["is_on"] is True - assert entity.get_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() @@ -208,8 +207,8 @@ async def test_fan( {"fan_mode": 2}, manufacturer=None ) # this is converted to a ranged value - assert entity.get_state()["percentage"] == 66 - assert entity.get_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() @@ -221,14 +220,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.get_state()["percentage"] == 33 # this is converted to a ranged value - assert entity.get_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.get_state()["percentage"] == 100 - assert entity.get_state()["speed"] == SPEED_HIGH + assert entity.state["percentage"] == 100 + assert entity.state["speed"] == SPEED_HIGH async def async_turn_on( @@ -318,7 +317,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.get_state()["is_on"] is False + assert entity.state["is_on"] is False # turn on from client group_fan_cluster.write_attributes.reset_mock() @@ -362,19 +361,19 @@ 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.get_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() # test that group fan is speed medium - assert entity.get_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() # test that group fan is now off - assert entity.get_state()["is_on"] is False + assert entity.state["is_on"] is False @patch( @@ -418,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.get_state()["is_on"] is False + assert entity.state["is_on"] is False # turn on from client group_fan_cluster.write_attributes.reset_mock() @@ -460,10 +459,10 @@ async def test_fan_init( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] == expected_state - assert entity.get_state()["speed"] == expected_speed - assert entity.get_state()["percentage"] == expected_percentage - assert entity.get_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( @@ -482,26 +481,26 @@ async def test_fan_update_entity( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] is False - assert entity.get_state()["speed"] == SPEED_OFF - assert entity.get_state()["percentage"] == 0 - assert entity.get_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.get_state()["is_on"] is False - assert entity.get_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.get_state()["is_on"] is True - assert entity.get_state()["percentage"] == 33 - assert entity.get_state()["speed"] == SPEED_LOW - assert entity.get_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 @@ -545,15 +544,15 @@ async def test_fan_ikea( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] is False + assert entity.state["is_on"] is False # turn on at fan await send_attributes_report(zha_gateway, cluster, {6: 1}) - assert entity.get_state()["is_on"] is True + assert entity.state["is_on"] is True # turn off at fan await send_attributes_report(zha_gateway, cluster, {6: 0}) - assert entity.get_state()["is_on"] is False + assert entity.state["is_on"] is False # turn on from HA cluster.write_attributes.reset_mock() @@ -633,9 +632,9 @@ async def test_fan_ikea_init( assert entity_id is not None entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] == ikea_expected_state - assert entity.get_state()["percentage"] == ikea_expected_percentage - assert entity.get_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( @@ -653,20 +652,20 @@ async def test_fan_ikea_update_entity( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] is False - assert entity.get_state()[ATTR_PERCENTAGE] == 0 - assert entity.get_state()[ATTR_PRESET_MODE] is None - assert entity.to_json()[ATTR_PERCENTAGE_STEP] == 100 / 10 + assert entity.state["is_on"] is False + assert entity.state[ATTR_PERCENTAGE] == 0 + assert entity.state[ATTR_PRESET_MODE] is None + assert entity.percentage_step == 100 / 10 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_state()["is_on"] is True - assert entity.get_state()[ATTR_PERCENTAGE] == 10 - assert entity.get_state()[ATTR_PRESET_MODE] is PRESET_MODE_AUTO - assert entity.to_json()[ATTR_PERCENTAGE_STEP] == 100 / 10 + assert entity.state["is_on"] is True + assert entity.state[ATTR_PERCENTAGE] == 10 + assert entity.state[ATTR_PRESET_MODE] is PRESET_MODE_AUTO + assert entity.percentage_step == 100 / 10 @pytest.fixture @@ -708,15 +707,15 @@ async def test_fan_kof( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_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.get_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.get_state()["is_on"] is False + assert entity.state["is_on"] is False # turn on from HA cluster.write_attributes.reset_mock() @@ -784,9 +783,9 @@ async def test_fan_kof_init( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] is expected_state - assert entity.get_state()[ATTR_PERCENTAGE] == expected_percentage - assert entity.get_state()[ATTR_PRESET_MODE] == expected_preset + assert entity.state["is_on"] is expected_state + assert entity.state[ATTR_PERCENTAGE] == expected_percentage + assert entity.state[ATTR_PRESET_MODE] == expected_preset async def test_fan_kof_update_entity( @@ -805,17 +804,17 @@ async def test_fan_kof_update_entity( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["is_on"] is False - assert entity.get_state()[ATTR_PERCENTAGE] == 0 - assert entity.get_state()[ATTR_PRESET_MODE] is None - assert entity.to_json()[ATTR_PERCENTAGE_STEP] == 100 / 4 + assert entity.state["is_on"] is False + assert entity.state[ATTR_PERCENTAGE] == 0 + assert entity.state[ATTR_PRESET_MODE] is None + assert entity.percentage_step == 100 / 4 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_state()["is_on"] is True - assert entity.get_state()[ATTR_PERCENTAGE] == 25 - assert entity.get_state()[ATTR_PRESET_MODE] is None - assert entity.to_json()[ATTR_PERCENTAGE_STEP] == 100 / 4 + assert entity.state["is_on"] is True + assert entity.state[ATTR_PERCENTAGE] == 25 + assert entity.state[ATTR_PRESET_MODE] is None + assert entity.percentage_step == 100 / 4 diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 79c26b5..61621b5 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -17,10 +17,14 @@ from tests.common import async_find_group_entity_id, find_entity_id from tests.conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from zha.application import Platform -from zha.application.gateway import Gateway +from zha.application.gateway import ( + Gateway, + RawDeviceInitializedDeviceInfo, + RawDeviceInitializedEvent, +) from zha.application.helpers import ZHAData from zha.application.platforms import GroupEntity, PlatformEntity -from zha.application.platforms.light.const import ColorMode, LightEntityFeature +from zha.application.platforms.light.const import LightEntityFeature from zha.zigbee.device import Device from zha.zigbee.group import Group, GroupMemberReference @@ -211,35 +215,16 @@ async def test_gateway_group_methods( assert isinstance(entity, GroupEntity) assert entity is not None - assert entity.to_json() == { - "class_name": "LightGroup", - "effect_list": None, - "group_id": zha_group.group_id, - "max_mireds": 500, - "min_mireds": 153, - "name": "Test Group_0x0002", - "platform": Platform.LIGHT, - "state": { - "brightness": None, - "class_name": "LightGroup", - "color_mode": ColorMode.UNKNOWN, - "color_temp": None, - "effect": None, - "hs_color": None, - "off_brightness": None, - "off_with_transition": False, - "on": False, - "supported_color_modes": { - ColorMode.BRIGHTNESS, - ColorMode.ONOFF, - ColorMode.XY, - }, - "supported_features": LightEntityFeature.TRANSITION, - "xy_color": None, - }, - "supported_features": LightEntityFeature.TRANSITION, - "unique_id": "light.0x0002", - } + info = entity.info_object + assert info.class_name == "LightGroup" + assert info.platform == Platform.LIGHT + assert info.unique_id == "light.0x0002" + assert info.name == "Test Group_0x0002" + assert info.group_id == zha_group.group_id + assert info.supported_features == LightEntityFeature.TRANSITION + assert info.min_mireds == 153 + assert info.max_mireds == 500 + assert info.effect_list is None device_1_entity_id = find_entity_id(Platform.LIGHT, device_light_1) device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2) @@ -543,15 +528,14 @@ def test_gateway_raw_device_initialized( assert zha_gateway.emit.call_count == 1 assert zha_gateway.emit.call_args == call( "raw_device_initialized", - { - "type": "zha_gateway_message", - "device_info": { - "nwk": 0xB79C, - "ieee": "00:0d:6f:00:0a:90:69:e7", - "pairing_status": "INTERVIEW_COMPLETE", - "model": "FakeModel", - "manufacturer": "FakeManufacturer", - "signature": { + RawDeviceInitializedEvent( + device_info=RawDeviceInitializedDeviceInfo( + ieee="00:0d:6f:00:0a:90:69:e7", + nwk=0xB79C, + pairing_status="INTERVIEW_COMPLETE", + model="FakeModel", + manufacturer="FakeManufacturer", + signature={ "manufacturer": "FakeManufacturer", "model": "FakeModel", "node_desc": { @@ -578,6 +562,8 @@ def test_gateway_raw_device_initialized( } }, }, - }, - }, + ), + event_type="zha_gateway_message", + event="raw_device_initialized", + ), ) diff --git a/tests/test_light.py b/tests/test_light.py index 0684c6f..8366b8b 100644 --- a/tests/test_light.py +++ b/tests/test_light.py @@ -295,7 +295,7 @@ async def test_light_refresh( entity = get_entity(zha_device, entity_id) assert entity is not None - assert bool(entity.get_state()["on"]) is False + assert bool(entity.state["on"]) is False on_off_cluster.read_attributes.reset_mock() @@ -304,7 +304,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.get_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} @@ -312,7 +312,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.get_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} @@ -320,7 +320,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.get_state()["on"]) is False + assert bool(entity.state["on"]) is False # TODO reporting is not checked @@ -384,7 +384,7 @@ async def test_light( entity = get_entity(zha_device, entity_id) assert entity is not None - assert bool(entity.get_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) @@ -429,13 +429,13 @@ async def test_light( if cluster_color: # test color temperature from the client with transition - assert entity.get_state()["brightness"] != 50 - assert entity.get_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.get_state()["brightness"] == 50 - assert entity.get_state()["color_temp"] == 200 - assert bool(entity.get_state()["on"]) is True + 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( @@ -451,11 +451,11 @@ async def test_light( cluster_color.request.reset_mock() # test color xy from the client - assert entity.get_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.get_state()["brightness"] == 50 - assert entity.get_state()["xy_color"] == [13369, 18087] + 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( @@ -482,12 +482,12 @@ async def async_test_on_off_from_light( # turn on at light await send_attributes_report(zha_gateway, cluster, {1: 0, 0: 1, 2: 3}) await zha_gateway.async_block_till_done() - assert bool(entity.get_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}) await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["on"]) is False + assert bool(entity.state["on"]) is False async def async_test_on_from_light( @@ -501,7 +501,7 @@ async def async_test_on_from_light( zha_gateway, cluster, {general.OnOff.AttributeDefs.on_off.id: 1} ) await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["on"]) is True + assert bool(entity.state["on"]) is True async def async_test_on_off_from_client( @@ -514,7 +514,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.get_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( @@ -540,7 +540,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.get_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( @@ -575,7 +575,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.get_state()["on"]) is False + assert bool(entity.state["on"]) is False await _reset_light() await _async_shift_time(zha_gateway) @@ -583,7 +583,7 @@ async def _reset_light(): # turn on via UI await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.get_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 @@ -602,7 +602,7 @@ async def _reset_light(): await entity.async_turn_on(transition=10) await zha_gateway.async_block_till_done() - assert bool(entity.get_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 @@ -622,7 +622,7 @@ async def _reset_light(): await entity.async_turn_on(brightness=10) await zha_gateway.async_block_till_done() - assert bool(entity.get_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 @@ -657,12 +657,12 @@ 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.get_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.get_state()["brightness"] is None + assert entity.state["brightness"] is None else: - assert entity.get_state()["brightness"] == level + assert entity.state["brightness"] == level async def async_test_flash_from_client( @@ -676,7 +676,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.get_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( @@ -785,7 +785,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.get_state()["on"]) is False + assert bool(entity.state["on"]) is False # test turning the lights on and off from the client await async_test_on_off_from_client(zha_gateway, group_cluster_on_off, entity) @@ -829,40 +829,40 @@ 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.get_state()["on"] is True - assert device_2_light_entity.get_state()["on"] is True - assert bool(entity.get_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.get_state()["on"] is False - assert device_2_light_entity.get_state()["on"] is True - assert bool(entity.get_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.get_state()["on"] is False - assert device_2_light_entity.get_state()["on"] is False - assert bool(entity.get_state()["on"]) is False + assert device_1_light_entity.state["on"] is False + assert device_2_light_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.get_state()["on"] is True - assert device_2_light_entity.get_state()["on"] is False - assert bool(entity.get_state()["on"]) is True + assert device_1_light_entity.state["on"] is True + assert device_2_light_entity.state["on"] is False + assert bool(entity.state["on"]) is True # 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.get_state()["on"] is False - assert device_2_light_entity.get_state()["on"] is False - assert bool(entity.get_state()["on"]) is False + assert device_1_light_entity.state["on"] is False + assert device_2_light_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( @@ -876,10 +876,10 @@ 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.get_state()["on"] is False - assert device_2_light_entity.get_state()["on"] is False - assert device_3_light_entity.get_state()["on"] is True - assert bool(entity.get_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 + 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( @@ -908,14 +908,14 @@ 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.get_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 await send_attributes_report(zha_gateway, dev1_cluster_on_off, {0: 0}) await send_attributes_report(zha_gateway, dev3_cluster_on_off, {0: 0}) await zha_gateway.async_block_till_done() - assert bool(entity.get_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( @@ -930,7 +930,7 @@ async def test_zha_group_light_entity( assert entity is not None await send_attributes_report(zha_gateway, dev2_cluster_on_off, {0: 1}) await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["on"]) is True + assert bool(entity.state["on"]) is True await zha_group.async_remove_members( [GroupMemberReference(ieee=coordinator.ieee, endpoint_id=1)] @@ -938,7 +938,7 @@ async def test_zha_group_light_entity( await zha_gateway.async_block_till_done() entity = get_group_entity(zha_group, group_entity_id) assert entity is not None - assert bool(entity.get_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 @@ -1116,9 +1116,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.get_state()["on"]) is False - assert bool(device_1_light_entity.get_state()["on"]) is False - assert bool(device_2_light_entity.get_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() @@ -1142,8 +1142,8 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is True - assert device_1_light_entity.get_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() @@ -1176,8 +1176,8 @@ async def test_transitions( tsn=None, ) - assert bool(eWeLink_light_entity.get_state()["on"]) is True - assert eWeLink_light_entity.get_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() @@ -1204,8 +1204,8 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is True - assert device_1_light_entity.get_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() @@ -1241,10 +1241,10 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is True - assert device_1_light_entity.get_state()["brightness"] == 18 - assert device_1_light_entity.get_state()["color_temp"] == 432 - assert device_1_light_entity.get_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() @@ -1269,7 +1269,7 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is False + assert bool(device_1_light_entity.state["on"]) is False dev1_cluster_level.request.reset_mock() @@ -1317,10 +1317,10 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is True - assert device_1_light_entity.get_state()["brightness"] == 25 - assert device_1_light_entity.get_state()["color_temp"] == 235 - assert device_1_light_entity.get_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() @@ -1335,7 +1335,7 @@ async def test_transitions( assert dev1_cluster_level.request.call_count == 0 assert dev1_cluster_level.request.await_count == 0 - assert bool(entity.get_state()["on"]) is False + assert bool(entity.state["on"]) is False dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1383,10 +1383,10 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is True - assert device_1_light_entity.get_state()["brightness"] == 25 - assert device_1_light_entity.get_state()["color_temp"] == 236 - assert device_1_light_entity.get_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() @@ -1400,7 +1400,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.get_state()["on"]) is False + assert bool(entity.state["on"]) is False dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1436,10 +1436,10 @@ async def test_transitions( tsn=None, ) - assert bool(device_1_light_entity.get_state()["on"]) is True - assert device_1_light_entity.get_state()["brightness"] == 25 - assert device_1_light_entity.get_state()["color_temp"] == 236 - assert device_1_light_entity.get_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() @@ -1453,7 +1453,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.get_state()["on"]) is False + assert bool(entity.state["on"]) is False dev1_cluster_on_off.request.reset_mock() dev1_cluster_color.request.reset_mock() @@ -1483,8 +1483,8 @@ async def test_transitions( tsn=None, ) - assert bool(device_2_light_entity.get_state()["on"]) is True - assert device_2_light_entity.get_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() @@ -1497,7 +1497,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.get_state()["on"]) is False + assert bool(device_2_light_entity.state["on"]) is False dev2_cluster_on_off.request.reset_mock() @@ -1545,10 +1545,10 @@ async def test_transitions( tsn=None, ) - assert bool(device_2_light_entity.get_state()["on"]) is True - assert device_2_light_entity.get_state()["brightness"] == 25 - assert device_2_light_entity.get_state()["color_temp"] == 235 - assert device_2_light_entity.get_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() @@ -1562,7 +1562,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.get_state()["on"]) is False + assert bool(device_2_light_entity.state["on"]) is False dev2_cluster_on_off.request.reset_mock() @@ -1602,10 +1602,10 @@ async def test_transitions( tsn=None, ) - assert bool(entity.get_state()["on"]) is True - assert entity.get_state()["brightness"] == 25 - assert entity.get_state()["color_temp"] == 235 - assert entity.get_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_handler.request.reset_mock() group_color_cluster_handler.request.reset_mock() @@ -1620,7 +1620,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.get_state()["on"]) is True + assert bool(device_2_light_entity.state["on"]) is True dev2_cluster_on_off.request.reset_mock() @@ -1644,7 +1644,7 @@ async def test_transitions( tsn=None, ) - assert bool(device_2_light_entity.get_state()["on"]) is False + assert bool(device_2_light_entity.state["on"]) is False dev2_cluster_level.request.reset_mock() @@ -1668,7 +1668,7 @@ async def test_transitions( tsn=None, ) - assert bool(device_2_light_entity.get_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() @@ -1705,11 +1705,11 @@ async def test_transitions( tsn=None, ) - assert bool(eWeLink_light_entity.get_state()["on"]) is True - assert eWeLink_light_entity.get_state()["color_temp"] == 235 - assert eWeLink_light_entity.get_state()["color_mode"] == ColorMode.COLOR_TEMP - assert eWeLink_light_entity.to_json()["min_mireds"] == 153 - assert eWeLink_light_entity.to_json()["max_mireds"] == 500 + 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 @patch( @@ -1777,9 +1777,9 @@ async def test_on_with_off_color( tsn=None, ) - assert bool(entity.get_state()["on"]) is True - assert entity.get_state()["color_temp"] == 235 - assert entity.get_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 # now let's turn off the Execute_if_off option and see if the old behavior is restored dev1_cluster_color.PLUGGED_ATTR_READS = {"options": 0} @@ -1834,10 +1834,10 @@ async def test_on_with_off_color( tsn=None, ) - assert bool(entity.get_state()["on"]) is True - assert entity.get_state()["color_temp"] == 240 - assert entity.get_state()["brightness"] == 254 - assert entity.get_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( @@ -1906,7 +1906,7 @@ async def test_group_member_assume_state( group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] # test that the lights were created and are off - assert bool(entity.get_state()["on"]) is False + assert bool(entity.state["on"]) is False group_cluster_on_off.request.reset_mock() await asyncio.sleep(11) @@ -1916,15 +1916,15 @@ async def test_group_member_assume_state( await zha_gateway.async_block_till_done() # members also instantly assume STATE_ON - assert bool(device_1_light_entity.get_state()["on"]) is True - assert bool(device_2_light_entity.get_state()["on"]) is True - assert bool(entity.get_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() await zha_gateway.async_block_till_done() # members also instantly assume STATE_OFF - assert bool(device_1_light_entity.get_state()["on"]) is False - assert bool(device_2_light_entity.get_state()["on"]) is False - assert bool(entity.get_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 diff --git a/tests/test_lock.py b/tests/test_lock.py index 1b2ed20..0c23bed 100644 --- a/tests/test_lock.py +++ b/tests/test_lock.py @@ -69,15 +69,15 @@ async def test_lock( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_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.get_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.get_state()["is_locked"] is False + assert entity.state["is_locked"] is False # lock from HA await async_lock(zha_gateway, cluster, entity) @@ -99,13 +99,13 @@ async def test_lock( # test updating entity state from client cluster.read_attributes.reset_mock() - assert entity.get_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.get_state()["is_locked"] is True + assert entity.state["is_locked"] is True async def async_lock( @@ -117,7 +117,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.get_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 @@ -127,7 +127,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.get_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 @@ -143,7 +143,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.get_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 @@ -153,7 +153,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.get_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 diff --git a/tests/test_number.py b/tests/test_number.py index e7500b9..4e130d4 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -110,23 +110,23 @@ async def test_number( assert cluster.read_attributes.call_count == 3 # test that the state is 15.0 - assert entity.get_state()["state"] == 15.0 + assert entity.state["state"] == 15.0 # test attributes - assert entity.to_json()["min_value"] == 1.0 - assert entity.to_json()["max_value"] == 100.0 - assert entity.to_json()["step"] == 1.1 + assert entity.info_object.min_value == 1.0 + assert entity.info_object.max_value == 100.0 + assert entity.info_object.step == 1.1 # change value from device assert cluster.read_attributes.call_count == 3 await send_attributes_report(zha_gateway, cluster, {0x0055: 15}) await zha_gateway.async_block_till_done() - assert entity.get_state()["state"] == 15.0 + assert entity.state["state"] == 15.0 # update value from device await send_attributes_report(zha_gateway, cluster, {0x0055: 20}) await zha_gateway.async_block_till_done() - assert entity.get_state()["state"] == 20.0 + assert entity.state["state"] == 20.0 # change value from client await entity.async_set_value(30.0) @@ -136,11 +136,11 @@ async def test_number( assert cluster.write_attributes.call_args == call( {"present_value": 30.0}, manufacturer=None ) - assert entity.get_state()["state"] == 30.0 + assert entity.state["state"] == 30.0 # test updating entity state from client cluster.read_attributes.reset_mock() - assert entity.get_state()["state"] == 30.0 + assert entity.state["state"] == 30.0 cluster.PLUGGED_ATTR_READS = {"present_value": 20} await entity.async_update() await zha_gateway.async_block_till_done() @@ -148,7 +148,7 @@ async def test_number( assert cluster.read_attributes.await_args == call( ["present_value"], allow_cache=False, only_cache=False, manufacturer=None ) - assert entity.get_state()["state"] == 20.0 + assert entity.state["state"] == 20.0 def get_entity(zha_dev: Device, entity_id: str) -> PlatformEntity: @@ -225,7 +225,7 @@ async def test_level_control_number( entity = get_entity(zha_device, entity_id) assert entity - assert entity.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value assert entity._attr_entity_category == EntityCategory.CONFIG await entity.async_set_native_value(new_value) @@ -233,12 +233,12 @@ async def test_level_control_number( call({attr: new_value}, manufacturer=None) ] - assert entity.get_state()["state"] == new_value + assert entity.state["state"] == 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.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value assert level_control_cluster.read_attributes.mock_calls == [ call( [attr], @@ -259,11 +259,11 @@ async def test_level_control_number( call({attr: new_value}, manufacturer=None), call({attr: new_value}, manufacturer=None), ] - assert entity.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value # test updating entity state from client level_control_cluster.read_attributes.reset_mock() - assert entity.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value level_control_cluster.PLUGGED_ATTR_READS = {attr: new_value} await entity.async_update() await zha_gateway.async_block_till_done() @@ -278,7 +278,7 @@ async def test_level_control_number( manufacturer=None, ), ] - assert entity.get_state()["state"] == new_value + assert entity.state["state"] == new_value @pytest.mark.parametrize( @@ -328,7 +328,7 @@ async def test_color_number( entity = get_entity(zha_device, entity_id) assert entity - assert entity.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value assert entity._attr_entity_category == EntityCategory.CONFIG await entity.async_set_native_value(new_value) @@ -337,12 +337,12 @@ async def test_color_number( attr: new_value, } - assert entity.get_state()["state"] == new_value + assert entity.state["state"] == 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.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value assert color_cluster.read_attributes.call_count == 1 assert ( call( @@ -365,11 +365,11 @@ async def test_color_number( call({attr: new_value}, manufacturer=None), call({attr: new_value}, manufacturer=None), ] - assert entity.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value # test updating entity state from client color_cluster.read_attributes.reset_mock() - assert entity.get_state()["state"] == initial_value + assert entity.state["state"] == initial_value color_cluster.PLUGGED_ATTR_READS = {attr: new_value} await entity.async_update() await zha_gateway.async_block_till_done() @@ -384,4 +384,4 @@ async def test_color_number( manufacturer=None, ), ] - assert entity.get_state()["state"] == new_value + assert entity.state["state"] == new_value diff --git a/tests/test_select.py b/tests/test_select.py index 30a9374..e1f5bf0 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -79,8 +79,8 @@ async def test_select( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["state"] is None # unknown in HA - assert entity.to_json()["options"] == [ + assert entity.state["state"] is None # unknown in HA + assert entity.info_object.options == [ "Stop", "Burglar", "Fire", @@ -94,9 +94,7 @@ async def test_select( # change value from client await entity.async_select_option(security.IasWd.Warning.WarningMode.Burglar.name) await zha_gateway.async_block_till_done() - assert ( - entity.get_state()["state"] == security.IasWd.Warning.WarningMode.Burglar.name - ) + assert entity.state["state"] == security.IasWd.Warning.WarningMode.Burglar.name class MotionSensitivityQuirk(CustomDevice): @@ -176,13 +174,13 @@ async def test_on_off_select_attribute_report( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["state"] == AqaraMotionSensitivities.Medium.name + assert entity.state["state"] == AqaraMotionSensitivities.Medium.name # send attribute report from device await send_attributes_report( zha_gateway, cluster, {"motion_sensitivity": AqaraMotionSensitivities.Low} ) - assert entity.get_state()["state"] == AqaraMotionSensitivities.Low.name + assert entity.state["state"] == AqaraMotionSensitivities.Low.name ( @@ -248,13 +246,13 @@ async def test_on_off_select_attribute_report_v2( assert entity is not None # test that the state is in default medium state - assert entity.get_state()["state"] == AqaraMotionSensitivities.Medium.name + assert entity.state["state"] == AqaraMotionSensitivities.Medium.name # send attribute report from device await send_attributes_report( zha_gateway, cluster, {"motion_sensitivity": AqaraMotionSensitivities.Low} ) - assert entity.get_state()["state"] == AqaraMotionSensitivities.Low.name + assert entity.state["state"] == AqaraMotionSensitivities.Low.name assert entity._attr_entity_category == EntityCategory.CONFIG # TODO assert entity._attr_entity_registry_enabled_default is True @@ -262,7 +260,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.get_state()["state"] == AqaraMotionSensitivities.Medium.name + assert entity.state["state"] == AqaraMotionSensitivities.Medium.name assert cluster.write_attributes.call_count == 1 assert cluster.write_attributes.call_args == call( {"motion_sensitivity": AqaraMotionSensitivities.Medium}, manufacturer=None diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 94f5e6a..5637e4f 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -17,7 +17,7 @@ from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from zha.application import Platform -from zha.application.const import ATTR_DEVICE_CLASS, ZHA_CLUSTER_HANDLER_READS_PER_REQ +from zha.application.const import ZHA_CLUSTER_HANDLER_READS_PER_REQ from zha.application.gateway import Gateway from zha.application.platforms import PlatformEntity from zha.application.platforms.sensor import UnitOfMass @@ -125,12 +125,12 @@ async def async_test_metering( zha_gateway, cluster, {1025: 1, 1024: 12345, 1026: 100} ) assert_state(entity, 12345.0, None) - assert entity.get_state()["status"] == "NO_ALARMS" - assert entity.get_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.get_state()["status"] in ( + assert entity.state["status"] in ( "SERVICE_DISCONNECT|POWER_FAILURE", "POWER_FAILURE|SERVICE_DISCONNECT", ) @@ -138,7 +138,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 1} ) - assert entity.get_state()["status"] in ( + assert entity.state["status"] in ( "SERVICE_DISCONNECT|NOT_DEFINED", "NOT_DEFINED|SERVICE_DISCONNECT", ) @@ -146,7 +146,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 2} ) - assert entity.get_state()["status"] in ( + assert entity.state["status"] in ( "SERVICE_DISCONNECT|PIPE_EMPTY", "PIPE_EMPTY|SERVICE_DISCONNECT", ) @@ -154,7 +154,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 5} ) - assert entity.get_state()["status"] in ( + assert entity.state["status"] in ( "SERVICE_DISCONNECT|TEMPERATURE_SENSOR", "TEMPERATURE_SENSOR|SERVICE_DISCONNECT", ) @@ -163,7 +163,7 @@ async def async_test_metering( await send_attributes_report( zha_gateway, cluster, {"status": 32, "metering_device_type": 4} ) - assert entity.get_state()["status"] in ("", "32") + assert entity.state["status"] in ("", "32") async def async_test_smart_energy_summation_delivered( @@ -175,9 +175,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.get_state()["status"] == "NO_ALARMS" - assert entity.get_state()["device_type"] == "Electric Metering" - assert entity.to_json()[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY + assert entity.state["status"] == "NO_ALARMS" + assert entity.state["device_type"] == "Electric Metering" + assert entity.info_object.device_class == SensorDeviceClass.ENERGY async def async_test_smart_energy_summation_received( @@ -189,9 +189,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.get_state()["status"] == "NO_ALARMS" - assert entity.get_state()["device_type"] == "Electric Metering" - assert entity.to_json()[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY + assert entity.state["status"] == "NO_ALARMS" + assert entity.state["device_type"] == "Electric Metering" + assert entity.info_object.device_class == SensorDeviceClass.ENERGY async def async_test_smart_energy_summation( @@ -203,8 +203,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.get_state()["status"] == "NO_ALARMS" - assert entity.get_state()["device_type"] == "Electric Metering" + assert entity.state["status"] == "NO_ALARMS" + assert entity.state["device_type"] == "Electric Metering" async def async_test_electrical_measurement( @@ -235,7 +235,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.get_state()["active_power_max"] == 8.8 + assert entity.state["active_power_max"] == 8.8 async def async_test_em_apparent_power( @@ -294,7 +294,7 @@ async def async_test_em_rms_current( assert_state(entity, 124, "A") await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050A: 88}) - assert entity.get_state()["rms_current_max"] == 8.8 + assert entity.state["rms_current_max"] == 8.8 async def async_test_em_rms_voltage( @@ -313,7 +313,7 @@ async def async_test_em_rms_voltage( assert_state(entity, 22.4, "V") await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0507: 888}) - assert entity.get_state()["rms_voltage_max"] == 8.9 + assert entity.state["rms_voltage_max"] == 8.9 async def async_test_powerconfiguration( @@ -322,11 +322,11 @@ async def async_test_powerconfiguration( """Test powerconfiguration/battery sensor.""" await send_attributes_report(zha_gateway, cluster, {33: 98}) assert_state(entity, 49, "%") - assert entity.get_state()["battery_voltage"] == 2.9 - assert entity.get_state()["battery_quantity"] == 3 - assert entity.get_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.get_state()["battery_voltage"] == 2.0 + assert entity.state["battery_voltage"] == 2.0 async def async_test_powerconfiguration2( @@ -360,7 +360,7 @@ async def async_test_setpoint_change_source( cluster, {hvac.Thermostat.AttributeDefs.setpoint_change_source.id: 0x01}, ) - assert entity.get_state()["state"] == "Schedule" + assert entity.state["state"] == "Schedule" async def async_test_pi_heating_demand( @@ -587,7 +587,7 @@ 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.get_state()["state"] == state + assert entity.state["state"] == state # TODO assert entity._attr_native_unit_of_measurement == unit_of_measurement @@ -625,7 +625,7 @@ async def test_electrical_measurement_init( cluster, {EMAttrs.active_power.id: 100}, ) - assert entity.get_state()["state"] == 100 + assert entity.state["state"] == 100 cluster_handler = list(zha_device._endpoints.values())[0].all_cluster_handlers[ "1:0x0b04" @@ -641,7 +641,7 @@ async def test_electrical_measurement_init( ) assert cluster_handler.ac_power_divisor == 5 assert cluster_handler.ac_power_multiplier == 1 - assert entity.get_state()["state"] == 4.0 + assert entity.state["state"] == 4.0 zha_device.available = False @@ -660,7 +660,7 @@ async def test_electrical_measurement_init( ) assert cluster_handler.ac_power_divisor == 10 assert cluster_handler.ac_power_multiplier == 1 - assert entity.get_state()["state"] == 3.0 + assert entity.state["state"] == 3.0 # update power multiplier await send_attributes_report( @@ -670,7 +670,7 @@ async def test_electrical_measurement_init( ) assert cluster_handler.ac_power_divisor == 10 assert cluster_handler.ac_power_multiplier == 6 - assert entity.get_state()["state"] == 12.0 + assert entity.state["state"] == 12.0 await send_attributes_report( zha_gateway, @@ -679,7 +679,7 @@ async def test_electrical_measurement_init( ) assert cluster_handler.ac_power_divisor == 10 assert cluster_handler.ac_power_multiplier == 20 - assert entity.get_state()["state"] == 60.0 + assert entity.state["state"] == 60.0 @pytest.mark.parametrize( @@ -960,7 +960,7 @@ async def test_elec_measurement_sensor_type( entity = get_entity(zha_dev, entity_id) assert entity is not None - assert entity.get_state()["measurement_type"] == expected_type + assert entity.state["measurement_type"] == expected_type @pytest.mark.looptime @@ -981,7 +981,7 @@ async def test_elec_measurement_sensor_polling( # pylint: disable=redefined-out # test that the sensor has an initial state of 2.0 entity = get_entity(zha_dev, entity_id) - assert entity.get_state()["state"] == 2.0 + assert entity.state["state"] == 2.0 # update the value for the power reading zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = ( @@ -989,14 +989,14 @@ async def test_elec_measurement_sensor_polling( # pylint: disable=redefined-out ) # ensure the state is still 2.0 - assert entity.get_state()["state"] == 2.0 + assert entity.state["state"] == 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.get_state()["state"] == 6.0 + assert entity.state["state"] == 6.0 @pytest.mark.parametrize( @@ -1166,7 +1166,7 @@ async def test_device_counter_sensors( entity = get_entity(coordinator, entity_id) assert entity is not None - assert entity.get_state()["state"] == 1 + assert entity.state["state"] == 1 # simulate counter increment on application coordinator.device.application.state.counters["ezsp_counters"][ @@ -1176,7 +1176,7 @@ async def test_device_counter_sensors( await entity.async_update() await zha_gateway.async_block_till_done() - assert entity.get_state()["state"] == 2 + assert entity.state["state"] == 2 coordinator.available = False await asyncio.sleep(120) diff --git a/tests/test_siren.py b/tests/test_siren.py index a090dab..c784a71 100644 --- a/tests/test_siren.py +++ b/tests/test_siren.py @@ -66,7 +66,7 @@ async def test_siren( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["state"] is False + assert entity.state["state"] is False # turn on from client with patch( @@ -85,7 +85,7 @@ async def test_siren( cluster.request.reset_mock() # test that the state has changed to on - assert entity.get_state()["state"] is True + assert entity.state["state"] is True # turn off from client with patch( @@ -104,7 +104,7 @@ async def test_siren( cluster.request.reset_mock() # test that the state has changed to off - assert entity.get_state()["state"] is False + assert entity.state["state"] is False # turn on from client with options with patch( @@ -123,7 +123,7 @@ async def test_siren( cluster.request.reset_mock() # test that the state has changed to on - assert entity.get_state()["state"] is True + assert entity.state["state"] is True @pytest.mark.looptime @@ -141,7 +141,7 @@ async def test_siren_timed_off( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.get_state()["state"] is False + assert entity.state["state"] is False # turn on from client with patch( @@ -160,9 +160,9 @@ async def test_siren_timed_off( cluster.request.reset_mock() # test that the state has changed to on - assert entity.get_state()["state"] is True + assert entity.state["state"] is True await asyncio.sleep(6) # test that the state has changed to off from the timer - assert entity.get_state()["state"] is False + assert entity.state["state"] is False diff --git a/tests/test_switch.py b/tests/test_switch.py index 10327e6..d161648 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -141,15 +141,15 @@ async def test_switch( entity: PlatformEntity = get_entity(zha_device, entity_id) assert entity is not None - assert bool(bool(entity.get_state()["state"])) is False + assert bool(bool(entity.state["state"])) is False # turn on at switch await send_attributes_report(zha_gateway, cluster, {1: 0, 0: 1, 2: 2}) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn off at switch await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 0, 2: 2}) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn on from client with patch( @@ -158,7 +158,7 @@ async def test_switch( ): await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -179,7 +179,7 @@ async def test_switch( ): await entity.async_turn_off() await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -197,7 +197,7 @@ async def test_switch( ): await entity.async_turn_off() await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -218,7 +218,7 @@ async def test_switch( ): await entity.async_turn_on() await zha_gateway.async_block_till_done() - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False assert len(cluster.request.mock_calls) == 1 assert cluster.request.call_args == call( False, @@ -231,7 +231,7 @@ async def test_switch( # test updating entity state from client cluster.read_attributes.reset_mock() - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False cluster.PLUGGED_ATTR_READS = {"on_off": True} await entity.async_update() await zha_gateway.async_block_till_done() @@ -239,7 +239,7 @@ async def test_switch( assert cluster.read_attributes.await_args == call( ["on_off"], allow_cache=False, only_cache=False, manufacturer=None ) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True async def test_zha_group_switch_entity( @@ -279,7 +279,7 @@ async def test_zha_group_switch_entity( dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off # test that the lights were created and are off - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn on from HA with patch( @@ -298,7 +298,7 @@ async def test_zha_group_switch_entity( manufacturer=None, tsn=None, ) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn off from HA with patch( @@ -317,7 +317,7 @@ async def test_zha_group_switch_entity( manufacturer=None, tsn=None, ) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) 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}) @@ -325,25 +325,25 @@ async def test_zha_group_switch_entity( await zha_gateway.async_block_till_done() # test that group light is on - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) 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.get_state()["state"]) is True + assert bool(entity.state["state"]) 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 bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) 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 bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True def get_entity(zha_dev: Device, entity_id: str) -> PlatformEntity: @@ -434,19 +434,19 @@ async def test_switch_configurable( assert entity is not None # test that the state has changed from unavailable to off - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn on at switch await send_attributes_report( zha_gateway, cluster, {"window_detection_function": True} ) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn off at switch await send_attributes_report( zha_gateway, cluster, {"window_detection_function": False} ) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn on from HA with patch( @@ -566,15 +566,15 @@ async def test_switch_configurable_custom_on_off_values( entity = get_entity(zha_device, entity_id) assert entity is not None - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn on at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 3}) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn off at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 5}) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn on from HA with patch( @@ -645,15 +645,15 @@ async def test_switch_configurable_custom_on_off_values_force_inverted( entity = get_entity(zha_device, entity_id) assert entity is not None - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn on at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 3}) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn off at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 5}) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn on from HA with patch( @@ -727,15 +727,15 @@ async def test_switch_configurable_custom_on_off_values_inverter_attribute( entity = get_entity(zha_device, entity_id) assert entity is not None - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn on at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 3}) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False # turn off at switch await send_attributes_report(zha_gateway, cluster, {"window_detection_function": 5}) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True # turn on from HA with patch( @@ -812,13 +812,13 @@ async def test_cover_inversion_switch( await entity.async_update() await zha_gateway.async_block_till_done() assert cluster.read_attributes.call_count == prev_call_count + 1 - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) 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.get_state()["state"]) is False + assert bool(entity.state["state"]) is False with patch( "zigpy.zcl.Cluster.write_attributes", return_value=[0x1, zcl_f.Status.SUCCESS] @@ -839,7 +839,7 @@ async def test_cover_inversion_switch( manufacturer=None, ) - assert bool(entity.get_state()["state"]) is True + assert bool(entity.state["state"]) is True cluster.write_attributes.reset_mock() @@ -855,7 +855,7 @@ async def test_cover_inversion_switch( manufacturer=None, ) - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False cluster.write_attributes.reset_mock() @@ -864,7 +864,7 @@ async def test_cover_inversion_switch( await zha_gateway.async_block_till_done() assert cluster.write_attributes.call_count == 0 - assert bool(entity.get_state()["state"]) is False + assert bool(entity.state["state"]) is False async def test_cover_inversion_switch_not_created( diff --git a/tests/test_update.py b/tests/test_update.py index 55202aa..cdfc227 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -120,7 +120,7 @@ async def test_firmware_update_notification_from_zigpy( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.state is False + assert entity.state["state"] is False # simulate an image available notification await cluster._handle_query_next_image( @@ -137,11 +137,11 @@ async def test_firmware_update_notification_from_zigpy( ) await zha_gateway.async_block_till_done() - assert entity.state is True - assert entity.get_state()[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" # type: ignore[unreachable] - assert not entity.get_state()[ATTR_IN_PROGRESS] + assert entity.state["state"] is True + assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not entity.state[ATTR_IN_PROGRESS] assert ( - entity.get_state()[ATTR_LATEST_VERSION] + entity.state[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -161,7 +161,7 @@ async def test_firmware_update_notification_from_service_call( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.state is False + assert entity.state["state"] is False # pylint: disable=pointless-string-statement """TODO @@ -189,7 +189,7 @@ async def _async_image_notify_side_effect(*args, **kwargs): ) await zha_gateway.async_block_till_done() - assert entity.state is True + assert entity.state["state"] is True assert entity.get_state()[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" assert not entity.get_state()[ATTR_IN_PROGRESS] assert ( @@ -250,7 +250,7 @@ async def test_firmware_update_success( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.state is False + assert entity.state["state"] is False # simulate an image available notification await cluster._handle_query_next_image( @@ -266,11 +266,11 @@ async def test_firmware_update_success( ) await zha_gateway.async_block_till_done() - assert entity.state is True - assert entity.get_state()[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" # type: ignore[unreachable] - assert not entity.get_state()[ATTR_IN_PROGRESS] + assert entity.state["state"] is True + assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not entity.state[ATTR_IN_PROGRESS] assert ( - entity.get_state()[ATTR_LATEST_VERSION] + entity.state[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -351,14 +351,14 @@ async def endpoint_reply(cluster_id, tsn, data, command_id): assert cmd.image_data == fw_image.firmware.serialize()[40:70] # make sure the state machine gets progress reports - assert entity.state is True + assert entity.state["state"] is True assert ( - entity.get_state()[ATTR_INSTALLED_VERSION] + entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" ) - assert entity.get_state()[ATTR_IN_PROGRESS] == 58 + assert entity.state[ATTR_IN_PROGRESS] == 58 assert ( - entity.get_state()[ATTR_LATEST_VERSION] + entity.state[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -404,23 +404,20 @@ def read_new_fw_version(*args, **kwargs): await entity.async_install(fw_image.firmware.header.file_version, False) await zha_gateway.async_block_till_done() - assert entity.state is False + assert entity.state["state"] is False assert ( - entity.get_state()[ATTR_INSTALLED_VERSION] + entity.state[ATTR_INSTALLED_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" ) - assert not entity.get_state()[ATTR_IN_PROGRESS] - assert ( - entity.get_state()[ATTR_LATEST_VERSION] - == entity.get_state()[ATTR_INSTALLED_VERSION] - ) + assert not entity.state[ATTR_IN_PROGRESS] + assert entity.state[ATTR_LATEST_VERSION] == entity.state[ATTR_INSTALLED_VERSION] # If we send a progress notification incorrectly, it won't be handled entity._update_progress(50, 100, 0.50) - assert not entity.get_state()[ATTR_IN_PROGRESS] - assert entity.state is False + assert not entity.state[ATTR_IN_PROGRESS] + assert entity.state["state"] is False async def test_firmware_update_raises( @@ -438,7 +435,7 @@ async def test_firmware_update_raises( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.state is False + assert entity.state["state"] is False # simulate an image available notification await cluster._handle_query_next_image( @@ -455,11 +452,11 @@ async def test_firmware_update_raises( ) await zha_gateway.async_block_till_done() - assert entity.state is True - assert entity.get_state()[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" # type: ignore[unreachable] - assert not entity.get_state()[ATTR_IN_PROGRESS] + assert entity.state["state"] is True + assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not entity.state[ATTR_IN_PROGRESS] assert ( - entity.get_state()[ATTR_LATEST_VERSION] + entity.state[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -520,7 +517,7 @@ async def test_firmware_update_no_longer_compatible( entity = get_entity(zha_device, entity_id) assert entity is not None - assert entity.state is False + assert entity.state["state"] is False # simulate an image available notification await cluster._handle_query_next_image( @@ -537,11 +534,11 @@ async def test_firmware_update_no_longer_compatible( ) await zha_gateway.async_block_till_done() - assert entity.state is True - assert entity.get_state()[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" # type: ignore[unreachable] - assert not entity.get_state()[ATTR_IN_PROGRESS] + assert entity.state["state"] is True + assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{installed_fw_version:08x}" + assert not entity.state[ATTR_IN_PROGRESS] assert ( - entity.get_state()[ATTR_LATEST_VERSION] + entity.state[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}" ) @@ -571,7 +568,7 @@ async def endpoint_reply(cluster_id, tsn, data, command_id): await zha_gateway.async_block_till_done() # We updated the currently installed firmware version, as it is no longer valid - assert entity.state is False - assert entity.get_state()[ATTR_INSTALLED_VERSION] == f"0x{new_version:08x}" - assert not entity.get_state()[ATTR_IN_PROGRESS] - assert entity.get_state()[ATTR_LATEST_VERSION] == f"0x{new_version:08x}" + assert entity.state["state"] is False + assert entity.state[ATTR_INSTALLED_VERSION] == f"0x{new_version:08x}" + assert not entity.state[ATTR_IN_PROGRESS] + assert entity.state[ATTR_LATEST_VERSION] == f"0x{new_version:08x}" diff --git a/zha/application/gateway.py b/zha/application/gateway.py index 5defa22..9eeb489 100644 --- a/zha/application/gateway.py +++ b/zha/application/gateway.py @@ -5,11 +5,13 @@ import asyncio from collections.abc import Iterable from contextlib import suppress +from dataclasses import dataclass from datetime import timedelta from enum import Enum +from functools import cached_property import logging import time -from typing import Final, Self, TypeVar, cast +from typing import Any, Final, Self, TypeVar, cast from zhaquirks import setup as setup_quirks from zigpy.application import ControllerApplication @@ -22,27 +24,18 @@ from zha.application import discovery from zha.application.const import ( - ATTR_IEEE, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NWK, - ATTR_SIGNATURE, - ATTR_TYPE, CONF_CUSTOM_QUIRKS_PATH, CONF_ENABLE_QUIRKS, CONF_RADIO_TYPE, CONF_USE_THREAD, CONF_ZIGPY, - DEVICE_PAIRING_STATUS, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA_GW_MSG, ZHA_GW_MSG_DEVICE_FULL_INIT, - ZHA_GW_MSG_DEVICE_INFO, ZHA_GW_MSG_DEVICE_JOINED, ZHA_GW_MSG_DEVICE_REMOVED, ZHA_GW_MSG_GROUP_ADDED, - ZHA_GW_MSG_GROUP_INFO, ZHA_GW_MSG_GROUP_MEMBER_ADDED, ZHA_GW_MSG_GROUP_MEMBER_REMOVED, ZHA_GW_MSG_GROUP_REMOVED, @@ -56,8 +49,8 @@ gather_with_limited_concurrency, ) from zha.event import EventBase -from zha.zigbee.device import Device, DeviceStatus -from zha.zigbee.group import Group, GroupMemberReference +from zha.zigbee.device import Device, DeviceInfo, DeviceStatus, ExtendedDeviceInfo +from zha.zigbee.group import Group, GroupInfo, GroupMemberReference BLOCK_LOG_TIMEOUT: Final[int] = 60 _R = TypeVar("_R") @@ -73,6 +66,83 @@ class DevicePairingStatus(Enum): INITIALIZED = 4 +@dataclass(kw_only=True, frozen=True) +class DeviceInfoWithPairingStatus(DeviceInfo): + """Information about a device with pairing status.""" + + pairing_status: DevicePairingStatus + + +@dataclass(kw_only=True, frozen=True) +class ExtendedDeviceInfoWithPairingStatus(ExtendedDeviceInfo): + """Information about a device with pairing status.""" + + pairing_status: DevicePairingStatus + + +@dataclass(kw_only=True, frozen=True) +class DeviceJoinedDeviceInfo: + """Information about a device.""" + + ieee: str + nwk: int + pairing_status: DevicePairingStatus + + +@dataclass(kw_only=True, frozen=True) +class DeviceJoinedEvent: + """Event to signal that a device has joined the network.""" + + device_info: DeviceJoinedDeviceInfo + event_type: Final[str] = ZHA_GW_MSG + event: Final[str] = ZHA_GW_MSG_DEVICE_JOINED + + +@dataclass(kw_only=True, frozen=True) +class RawDeviceInitializedDeviceInfo(DeviceJoinedDeviceInfo): + """Information about a device that has been initialized without quirks loaded.""" + + model: str + manufacturer: str + signature: dict[str, Any] + + +@dataclass(kw_only=True, frozen=True) +class RawDeviceInitializedEvent: + """Event to signal that a device has been initialized without quirks loaded.""" + + device_info: RawDeviceInitializedDeviceInfo + event_type: Final[str] = ZHA_GW_MSG + event: Final[str] = ZHA_GW_MSG_RAW_INIT + + +@dataclass(kw_only=True, frozen=True) +class DeviceFullInitEvent: + """Event to signal that a device has been fully initialized.""" + + device_info: ExtendedDeviceInfoWithPairingStatus + event_type: Final[str] = ZHA_GW_MSG + event: Final[str] = ZHA_GW_MSG_DEVICE_FULL_INIT + + +@dataclass(kw_only=True, frozen=True) +class GroupEvent: + """Event to signal a group event.""" + + event: str + group_info: GroupInfo + event_type: Final[str] = ZHA_GW_MSG + + +@dataclass(kw_only=True, frozen=True) +class DeviceRemovedEvent: + """Event to signal that a device has been removed.""" + + device_info: ExtendedDeviceInfo + event_type: Final[str] = ZHA_GW_MSG + event: Final[str] = ZHA_GW_MSG_DEVICE_REMOVED + + class Gateway(AsyncUtilMixin, EventBase): """Gateway that handles events that happen on the ZHA Zigbee network.""" @@ -227,10 +297,12 @@ def create_platform_entities(self) -> None: *args, **kw_args ) if platform_entity: - _LOGGER.debug("Platform entity data: %s", platform_entity.to_json()) + _LOGGER.debug( + "Platform entity data: %s", platform_entity.info_object + ) self.config.platforms[platform].clear() - @property + @cached_property def radio_concurrency(self) -> int: """Maximum configured radio concurrency.""" return self.application_controller._concurrent_requests_semaphore.max_value # pylint: disable=protected-access @@ -291,14 +363,13 @@ def device_joined(self, device: zigpy.device.Device) -> None: self.emit( ZHA_GW_MSG_DEVICE_JOINED, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_INFO: { - ATTR_NWK: device.nwk, - ATTR_IEEE: str(device.ieee), - DEVICE_PAIRING_STATUS: DevicePairingStatus.PAIRED.name, - }, - }, + DeviceJoinedEvent( + device_info=DeviceJoinedDeviceInfo( + ieee=str(device.ieee), + nwk=device.nwk, + pairing_status=DevicePairingStatus.PAIRED.name, + ) + ), ) def raw_device_initialized(self, device: zigpy.device.Device) -> None: # pylint: disable=unused-argument @@ -306,19 +377,18 @@ def raw_device_initialized(self, device: zigpy.device.Device) -> None: # pylint self.emit( ZHA_GW_MSG_RAW_INIT, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_INFO: { - ATTR_NWK: device.nwk, - ATTR_IEEE: str(device.ieee), - DEVICE_PAIRING_STATUS: DevicePairingStatus.INTERVIEW_COMPLETE.name, - ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL, - ATTR_MANUFACTURER: device.manufacturer + RawDeviceInitializedEvent( + device_info=RawDeviceInitializedDeviceInfo( + ieee=str(device.ieee), + nwk=device.nwk, + pairing_status=DevicePairingStatus.INTERVIEW_COMPLETE.name, + model=device.model if device.model else UNKNOWN_MODEL, + manufacturer=device.manufacturer if device.manufacturer else UNKNOWN_MANUFACTURER, - ATTR_SIGNATURE: device.get_signature(), - }, - }, + signature=device.get_signature(), + ) + ), ) def device_initialized(self, device: zigpy.device.Device) -> None: @@ -346,6 +416,7 @@ def group_member_removed( """Handle zigpy group member removed event.""" # need to handle endpoint correctly on groups zha_group = self.get_or_create_group(zigpy_group) + zha_group.clear_caches() discovery.GROUP_PROBE.discover_group_entities(zha_group) zha_group.info("group_member_removed - endpoint: %s", endpoint) self._emit_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) @@ -356,6 +427,7 @@ def group_member_added( """Handle zigpy group member added event.""" # need to handle endpoint correctly on groups zha_group = self.get_or_create_group(zigpy_group) + zha_group.clear_caches() discovery.GROUP_PROBE.discover_group_entities(zha_group) zha_group.info("group_member_added - endpoint: %s", endpoint) self._emit_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) @@ -383,10 +455,10 @@ def _emit_group_gateway_message( # pylint: disable=unused-argument if zha_group is not None: self.emit( gateway_message_type, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_GROUP_INFO: zha_group.group_info, - }, + GroupEvent( + event=gateway_message_type, + group_info=zha_group.info_object, + ), ) def device_removed(self, device: zigpy.device.Device) -> None: @@ -394,7 +466,7 @@ def device_removed(self, device: zigpy.device.Device) -> None: _LOGGER.info("Removing device %s - %s", device.ieee, f"0x{device.nwk:04x}") zha_device = self._devices.pop(device.ieee, None) if zha_device is not None: - device_info = zha_device.zha_device_info + device_info = zha_device.extended_device_info self.track_task( create_eager_task( zha_device.on_remove(), name="Gateway._async_remove_device" @@ -403,10 +475,7 @@ def device_removed(self, device: zigpy.device.Device) -> None: if device_info is not None: self.emit( ZHA_GW_MSG_DEVICE_REMOVED, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, + DeviceRemovedEvent(device_info=device_info), ) def get_device(self, ieee: EUI64) -> Device | None: @@ -489,27 +558,25 @@ async def async_device_initialized(self, device: zigpy.device.Device) -> None: ) await self._async_device_joined(zha_device) - device_info = zha_device.zha_device_info - device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.INITIALIZED.name + device_info = ExtendedDeviceInfoWithPairingStatus( + pairing_status=DevicePairingStatus.INITIALIZED.name, + **zha_device.extended_device_info.__dict__, + ) self.emit( ZHA_GW_MSG_DEVICE_FULL_INIT, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, + DeviceFullInitEvent(device_info=device_info), ) async def _async_device_joined(self, zha_device: Device) -> None: zha_device.available = True - device_info = zha_device.device_info await zha_device.async_configure() - device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name + device_info = ExtendedDeviceInfoWithPairingStatus( + pairing_status=DevicePairingStatus.CONFIGURED.name, + **zha_device.extended_device_info.__dict__, + ) self.emit( ZHA_GW_MSG_DEVICE_FULL_INIT, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, + DeviceFullInitEvent(device_info=device_info), ) await zha_device.async_initialize(from_cache=False) self.create_platform_entities() @@ -523,14 +590,13 @@ async def _async_device_rejoined(self, zha_device: Device) -> None: # we don't have to do this on a nwk swap # but we don't have a way to tell currently await zha_device.async_configure() - device_info = zha_device.device_info - device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name + device_info = ExtendedDeviceInfoWithPairingStatus( + pairing_status=DevicePairingStatus.CONFIGURED.name, + **zha_device.extended_device_info.__dict__, + ) self.emit( ZHA_GW_MSG_DEVICE_FULL_INIT, - { - ATTR_TYPE: ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, + DeviceFullInitEvent(device_info=device_info), ) # force async_initialize() to fire so don't explicitly call it zha_device.available = False diff --git a/zha/application/platforms/__init__.py b/zha/application/platforms/__init__.py index cd0fd55..3f288ce 100644 --- a/zha/application/platforms/__init__.py +++ b/zha/application/platforms/__init__.py @@ -2,13 +2,14 @@ from __future__ import annotations -import abc +from abc import abstractmethod import asyncio from contextlib import suppress from dataclasses import dataclass from enum import StrEnum +from functools import cached_property import logging -from typing import TYPE_CHECKING, Any, Final, Optional +from typing import TYPE_CHECKING, Any, Final, Optional, final from zigpy.quirks.v2 import EntityMetadata, EntityType from zigpy.types.named import EUI64 @@ -17,6 +18,7 @@ from zha.const import STATE_CHANGED from zha.event import EventBase from zha.mixins import LogMixin +from zha.zigbee.cluster_handlers import ClusterHandlerInfo if TYPE_CHECKING: from zha.zigbee.cluster_handlers import ClusterHandler @@ -39,6 +41,57 @@ class EntityCategory(StrEnum): DIAGNOSTIC = "diagnostic" +@dataclass(frozen=True, kw_only=True) +class BaseEntityInfo: + """Information about a base entity.""" + + unique_id: str + platform: str + class_name: str + + +@dataclass(frozen=True, kw_only=True) +class BaseIdentifiers: + """Identifiers for the base entity.""" + + unique_id: str + platform: str + + +@dataclass(frozen=True, kw_only=True) +class PlatformEntityIdentifiers(BaseIdentifiers): + """Identifiers for the platform entity.""" + + device_ieee: EUI64 + endpoint_id: int + + +@dataclass(frozen=True, kw_only=True) +class GroupEntityIdentifiers(BaseIdentifiers): + """Identifiers for the group entity.""" + + group_id: int + + +@dataclass(frozen=True, kw_only=True) +class PlatformEntityInfo(BaseEntityInfo): + """Information about a platform entity.""" + + name: str + cluster_handlers: list[ClusterHandlerInfo] + device_ieee: EUI64 + endpoint_id: int + available: bool + + +@dataclass(frozen=True, kw_only=True) +class GroupEntityInfo(BaseEntityInfo): + """Information about a group entity.""" + + name: str + group_id: int + + @dataclass(frozen=True, kw_only=True) class EntityStateChangedEvent: """Event for when an entity state changes.""" @@ -67,28 +120,39 @@ def __init__(self, unique_id: str, **kwargs: Any) -> None: self._unique_id: str = unique_id if self._unique_id_suffix: self._unique_id += f"-{self._unique_id_suffix}" - self._state: Any = None - self._previous_state: Any = None + self.__previous_state: Any = None self._tracked_tasks: list[asyncio.Task] = [] - @property + @final + @cached_property def unique_id(self) -> str: """Return the unique id.""" return self._unique_id - @abc.abstractmethod - def get_identifiers(self) -> dict[str, str | int]: + @cached_property + def identifiers(self) -> BaseIdentifiers: """Return a dict with the information necessary to identify this entity.""" + return BaseIdentifiers( + unique_id=self.unique_id, + platform=self.PLATFORM, + ) + + @cached_property + def info_object(self) -> BaseEntityInfo: + """Return a representation of the platform entity.""" + return BaseEntityInfo( + unique_id=self._unique_id, + platform=self.PLATFORM, + class_name=self.__class__.__name__, + ) - def get_state(self) -> dict: + @property + def state(self) -> dict[str, Any]: """Return the arguments to use in the command.""" return { "class_name": self.__class__.__name__, } - async def async_update(self) -> None: - """Retrieve latest state.""" - async def on_remove(self) -> None: """Cancel tasks this entity owns.""" tasks = [t for t in self._tracked_tasks if not (t.done() or t.cancelled())] @@ -100,19 +164,12 @@ async def on_remove(self) -> None: def maybe_emit_state_changed_event(self) -> None: """Send the state of this platform entity.""" - state = self.get_state() - if self._previous_state != state: - self.emit(STATE_CHANGED, EntityStateChangedEvent(**self.get_identifiers())) - self._previous_state = state - - def to_json(self) -> dict: - """Return a JSON representation of the platform entity.""" - return { - "unique_id": self._unique_id, - "platform": self.PLATFORM, - "class_name": self.__class__.__name__, - "state": self.get_state(), - } + state = self.state + if self.__previous_state != state: + self.emit( + STATE_CHANGED, EntityStateChangedEvent(**self.identifiers.__dict__) + ) + self.__previous_state = state def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None: """Log a message.""" @@ -197,17 +254,41 @@ def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: else: self._attr_entity_category = None - @property + @cached_property + def identifiers(self) -> PlatformEntityIdentifiers: + """Return a dict with the information necessary to identify this entity.""" + return PlatformEntityIdentifiers( + unique_id=self.unique_id, + platform=self.PLATFORM, + device_ieee=self.device.ieee, + endpoint_id=self.endpoint.id, + ) + + @cached_property + def info_object(self) -> PlatformEntityInfo: + """Return a representation of the platform entity.""" + return PlatformEntityInfo( + unique_id=self._unique_id, + platform=self.PLATFORM, + class_name=self.__class__.__name__, + name=self._name, + cluster_handlers=[ch.info_object for ch in self._cluster_handlers], + device_ieee=self._device.ieee, + endpoint_id=self._endpoint.id, + available=self.available, + ) + + @cached_property def device(self) -> Device: """Return the device.""" return self._device - @property + @cached_property def endpoint(self) -> Endpoint: """Return the endpoint.""" return self._endpoint - @property + @cached_property def should_poll(self) -> bool: """Return True if we need to poll for state changes.""" return False @@ -222,28 +303,10 @@ def name(self) -> str: """Return the name of the platform entity.""" return self._name - def get_identifiers(self) -> dict[str, str | int]: - """Return a dict with the information necessary to identify this entity.""" - return { - "unique_id": self.unique_id, - "platform": self.PLATFORM, - "device_ieee": self.device.ieee, - "endpoint_id": self.endpoint.id, - } - - def to_json(self) -> dict: - """Return a JSON representation of the platform entity.""" - json = super().to_json() - json["name"] = self._name - json["cluster_handlers"] = [ch.to_json() for ch in self._cluster_handlers] - json["device_ieee"] = str(self._device.ieee) - json["endpoint_id"] = self._endpoint.id - json["available"] = self.available - return json - - def get_state(self) -> dict: + @property + def state(self) -> dict[str, Any]: """Return the arguments to use in the command.""" - state = super().get_state() + state = super().state state["available"] = self.available return state @@ -272,7 +335,26 @@ def __init__( self._name: str = f"{group.name}_0x{group.group_id:04x}" self._group: Group = group self._group.register_group_entity(self) - self.update() + + @cached_property + def identifiers(self) -> GroupEntityIdentifiers: + """Return a dict with the information necessary to identify this entity.""" + return GroupEntityIdentifiers( + unique_id=self.unique_id, + platform=self.PLATFORM, + group_id=self.group_id, + ) + + @cached_property + def info_object(self) -> GroupEntityInfo: + """Return a representation of the group.""" + return GroupEntityInfo( + unique_id=self._unique_id, + platform=self.PLATFORM, + class_name=self.__class__.__name__, + name=self._name, + group_id=self.group_id, + ) @property def name(self) -> str: @@ -284,25 +366,11 @@ def group_id(self) -> int: """Return the group id.""" return self._group.group_id - @property + @cached_property def group(self) -> Group: """Return the group.""" return self._group - def get_identifiers(self) -> dict[str, str | int]: - """Return a dict with the information necessary to identify this entity.""" - return { - "unique_id": self.unique_id, - "platform": self.PLATFORM, - "group_id": self.group.group_id, - } - + @abstractmethod def update(self, _: Any | None = None) -> None: """Update the state of this group entity.""" - - def to_json(self) -> dict[str, Any]: - """Return a JSON representation of the group.""" - json = super().to_json() - json["name"] = self._name - json["group_id"] = self.group_id - return json diff --git a/zha/application/platforms/alarm_control_panel/__init__.py b/zha/application/platforms/alarm_control_panel/__init__.py index cd80ff6..4c2e293 100644 --- a/zha/application/platforms/alarm_control_panel/__init__.py +++ b/zha/application/platforms/alarm_control_panel/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations +from dataclasses import dataclass import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from zigpy.zcl.clusters.security import IasAce @@ -16,7 +17,7 @@ ZHA_ALARM_OPTIONS, ) from zha.application.helpers import async_get_zha_config_value -from zha.application.platforms import PlatformEntity +from zha.application.platforms import PlatformEntity, PlatformEntityInfo from zha.application.platforms.alarm_control_panel.const import ( IAS_ACE_STATE_MAP, SUPPORT_ALARM_ARM_AWAY, @@ -48,6 +49,16 @@ _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class AlarmControlPanelEntityInfo(PlatformEntityInfo): + """Alarm control panel entity info.""" + + code_arm_required: bool + code_format: CodeFormat + supported_features: int + translation_key: str + + @STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_ACE) class AlarmControlPanel(PlatformEntity): """Entity for ZHA alarm control devices.""" @@ -80,28 +91,58 @@ def __init__( CLUSTER_HANDLER_STATE_CHANGED, self._handle_event_protocol ) - def handle_cluster_handler_state_changed( - self, - event: ClusterHandlerStateChangedEvent, # pylint: disable=unused-argument - ) -> None: - """Handle state changed on cluster.""" - self.maybe_emit_state_changed_event() + @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, + translation_key=self.translation_key, + ) + + @property + def state(self) -> dict[str, Any]: + """Get the state of the alarm control panel.""" + response = super().state + response["state"] = IAS_ACE_STATE_MAP.get( + self._cluster_handler.armed_state, AlarmState.UNKNOWN + ) + return response @property def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" return self._cluster_handler.code_required_arm_actions - @property + @functools.cached_property def code_format(self) -> CodeFormat: """Code format or None if no code is required.""" return CodeFormat.NUMBER - @property + @functools.cached_property def translation_key(self) -> str: """Return the translation key.""" return self._attr_translation_key + @functools.cached_property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + + def handle_cluster_handler_state_changed( + self, + event: ClusterHandlerStateChangedEvent, # pylint: disable=unused-argument + ) -> None: + """Handle state changed on cluster.""" + self.maybe_emit_state_changed_event() + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self._cluster_handler.arm(IasAce.ArmMode.Disarm, code, 0) @@ -126,35 +167,3 @@ async def async_alarm_trigger(self, code: str | None = None) -> None: # pylint: """Send alarm trigger command.""" self._cluster_handler.panic() self.maybe_emit_state_changed_event() - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return ( - SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY - | SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_TRIGGER - ) - - def to_json(self) -> dict: - """Return a JSON representation of the alarm control panel.""" - json = super().to_json() - json["supported_features"] = self.supported_features - json["code_arm_required"] = self.code_arm_required - json["code_format"] = self.code_format - json["translation_key"] = self.translation_key - return json - - @property - def state(self) -> str: - """Return the state of the entity.""" - return IAS_ACE_STATE_MAP.get( - self._cluster_handler.armed_state, AlarmState.UNKNOWN - ) - - def get_state(self) -> dict: - """Get the state of the alarm control panel.""" - response = super().get_state() - response["state"] = self.state - return response diff --git a/zha/application/platforms/binary_sensor/__init__.py b/zha/application/platforms/binary_sensor/__init__.py index f55a12e..b8b87a4 100644 --- a/zha/application/platforms/binary_sensor/__init__.py +++ b/zha/application/platforms/binary_sensor/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import functools import logging from typing import TYPE_CHECKING @@ -10,7 +11,7 @@ from zha.application import Platform from zha.application.const import ATTR_DEVICE_CLASS, ENTITY_METADATA -from zha.application.platforms import EntityCategory, PlatformEntity +from zha.application.platforms import EntityCategory, PlatformEntity, PlatformEntityInfo from zha.application.platforms.binary_sensor.const import ( IAS_ZONE_CLASS_MAPPING, BinarySensorDeviceClass, @@ -44,6 +45,14 @@ _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class BinarySensorEntityInfo(PlatformEntityInfo): + """Binary sensor entity info.""" + + attribute_name: str + device_class: BinarySensorDeviceClass | None + + class BinarySensor(PlatformEntity): """ZHA BinarySensor.""" @@ -64,6 +73,7 @@ def __init__( if ENTITY_METADATA in kwargs: self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, cluster_handlers, endpoint, device, **kwargs) + self._state: bool = self.is_on self._cluster_handler.on_event( CLUSTER_HANDLER_ATTRIBUTE_UPDATED, self.handle_cluster_handler_attribute_updated, @@ -81,25 +91,39 @@ def _init_from_quirks_metadata(self, entity_metadata: BinarySensorMetadata) -> N _LOGGER, ) + @functools.cached_property + def info_object(self) -> dict: + """Return a representation of the binary sensor.""" + return BinarySensorEntityInfo( + **super().info_object.__dict__, + attribute_name=self._attribute_name, + device_class=self._attr_device_class + if hasattr(self, ATTR_DEVICE_CLASS) + else None, + ) + + @property + def state(self) -> dict: + """Return the state of the binary sensor.""" + response = super().state + response["state"] = self.is_on + return response + @property def is_on(self) -> bool: """Return True if the switch is on based on the state machine.""" - raw_state = self._cluster_handler.cluster.get(self._attribute_name) + self._state = raw_state = self._cluster_handler.cluster.get( + self._attribute_name + ) if raw_state is None: return False return self.parse(raw_state) - @property + @functools.cached_property def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this entity.""" return self._attr_device_class - def get_state(self) -> dict: - """Return the state of the binary sensor.""" - response = super().get_state() - response["state"] = self.is_on - return response - def handle_cluster_handler_attribute_updated( self, event: ClusterAttributeUpdatedEvent ) -> None: @@ -119,14 +143,6 @@ async def async_update(self) -> None: self._state = attr_value self.maybe_emit_state_changed_event() - def to_json(self) -> dict: - """Return a JSON representation of the binary sensor.""" - json = super().to_json() - json["sensor_attribute"] = self._attribute_name - if hasattr(self, ATTR_DEVICE_CLASS): - json[ATTR_DEVICE_CLASS] = self._attr_device_class - return json - @staticmethod def parse(value: bool | int) -> bool: """Parse the raw attribute into a bool state.""" @@ -223,7 +239,7 @@ def __init__( self._attr_device_class = self.device_class self._attr_translation_key = self.translation_key - @property + @functools.cached_property def translation_key(self) -> str | None: """Return the name of the sensor.""" zone_type = self._cluster_handler.cluster.get("zone_type") @@ -231,7 +247,7 @@ def translation_key(self) -> str | None: return None return "ias_zone" - @property + @functools.cached_property def device_class(self) -> BinarySensorDeviceClass | None: """Return device class from platform DEVICE_CLASSES.""" zone_type = self._cluster_handler.cluster.get("zone_type") diff --git a/zha/application/platforms/button/__init__.py b/zha/application/platforms/button/__init__.py index f08bca2..4d20e9d 100644 --- a/zha/application/platforms/button/__init__.py +++ b/zha/application/platforms/button/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import functools import logging from typing import TYPE_CHECKING, Any, Self @@ -10,7 +11,7 @@ from zha.application import Platform from zha.application.const import ENTITY_METADATA -from zha.application.platforms import EntityCategory, PlatformEntity +from zha.application.platforms import EntityCategory, PlatformEntity, PlatformEntityInfo from zha.application.platforms.button.const import DEFAULT_DURATION, ButtonDeviceClass from zha.application.registries import PLATFORM_ENTITIES from zha.zigbee.cluster_handlers.const import CLUSTER_HANDLER_IDENTIFY @@ -29,6 +30,23 @@ _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class CommandButtonEntityInfo(PlatformEntityInfo): + """Command button entity info.""" + + command: str + args: list[Any] + kwargs: dict[str, Any] + + +@dataclass(frozen=True, kw_only=True) +class WriteAttributeButtonEntityInfo(PlatformEntityInfo): + """Write attribute button entity info.""" + + attribute_name: str + attribute_value: Any + + class Button(PlatformEntity): """Defines a ZHA button.""" @@ -61,27 +79,33 @@ def _init_from_quirks_metadata( self._args = entity_metadata.args self._kwargs = entity_metadata.kwargs - def get_args(self) -> list[Any]: + @functools.cached_property + def info_object(self) -> CommandButtonEntityInfo: + """Return a representation of the button.""" + return CommandButtonEntityInfo( + **super().info_object.__dict__, + command=self._command_name, + args=self._args, + kwargs=self._kwargs, + ) + + @functools.cached_property + def args(self) -> list[Any]: """Return the arguments to use in the command.""" return list(self._args) if self._args else [] - def get_kwargs(self) -> dict[str, Any]: + @functools.cached_property + def kwargs(self) -> dict[str, Any]: """Return the keyword arguments to use in the command.""" return self._kwargs async def async_press(self) -> None: """Send out a update command.""" command = getattr(self._cluster_handler, self._command_name) - arguments = self.get_args() or [] - kwargs = self.get_kwargs() or {} + arguments = self.args or [] + kwargs = self.kwargs or {} await command(*arguments, **kwargs) - def to_json(self) -> dict: - """Return a JSON representation of the button.""" - json = super().to_json() - json["command"] = self._command_name - return json - @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IDENTIFY) class IdentifyButton(Button): @@ -143,6 +167,15 @@ def _init_from_quirks_metadata( self._attribute_name = entity_metadata.attribute_name self._attribute_value = entity_metadata.attribute_value + @functools.cached_property + def info_object(self) -> WriteAttributeButtonEntityInfo: + """Return a representation of the button.""" + return WriteAttributeButtonEntityInfo( + **super().info_object.__dict__, + attribute_name=self._attribute_name, + attribute_value=self._attribute_value, + ) + async def async_press(self) -> None: """Write attribute with defined value.""" await self._cluster_handler.write_attributes_safe( diff --git a/zha/application/platforms/climate/__init__.py b/zha/application/platforms/climate/__init__.py index a269f35..bc14442 100644 --- a/zha/application/platforms/climate/__init__.py +++ b/zha/application/platforms/climate/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import datetime as dt import functools from typing import TYPE_CHECKING, Any @@ -9,7 +10,7 @@ from zigpy.zcl.clusters.hvac import FanMode, RunningState, SystemMode from zha.application import Platform -from zha.application.platforms import PlatformEntity +from zha.application.platforms import PlatformEntity, PlatformEntityInfo from zha.application.platforms.climate.const import ( ATTR_HVAC_MODE, ATTR_OCCP_COOL_SETPT, @@ -54,6 +55,18 @@ MULTI_MATCH = functools.partial(PLATFORM_ENTITIES.multipass_match, Platform.CLIMATE) +@dataclass(frozen=True, kw_only=True) +class ThermostatEntityInfo(PlatformEntityInfo): + """Thermostat entity info.""" + + max_temp: float + min_temp: float + supported_features: ClimateEntityFeature + fan_modes: list[str] | None + preset_modes: list[str] | None + hvac_modes: list[HVACMode] + + @MULTI_MATCH( cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, aux_cluster_handlers=CLUSTER_HANDLER_FAN, @@ -99,6 +112,34 @@ def __init__( self.handle_cluster_handler_attribute_updated, ) + @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) -> dict[str, Any]: + """Get the state of the lock.""" + response = super().state + response["current_temperature"] = self.current_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 + response.update(self.extra_state_attributes) + return response + @property def current_temperature(self): """Return the current temperature.""" @@ -163,7 +204,7 @@ def fan_mode(self) -> str | None: return FAN_ON return FAN_AUTO - @property + @functools.cached_property def fan_modes(self) -> list[str] | None: """Return supported FAN modes.""" if not self._fan_cluster_handler: @@ -226,7 +267,7 @@ def hvac_mode(self) -> HVACMode | None: """Return HVAC operation mode.""" return SYSTEM_MODE_2_HVAC.get(self._thermostat_cluster_handler.system_mode) - @property + @functools.cached_property def hvac_modes(self) -> list[HVACMode]: """Return the list of available HVAC operation modes.""" return SEQ_OF_OPERATION.get( @@ -238,12 +279,12 @@ def preset_mode(self) -> str: """Return current preset mode.""" return self._preset - @property + @functools.cached_property def preset_modes(self) -> list[str] | None: """Return supported preset modes.""" return self._presets - @property + @functools.cached_property def supported_features(self) -> ClimateEntityFeature: """Return the list of supported features.""" features = self._supported_flags @@ -437,31 +478,6 @@ async def async_preset_handler(self, preset: str, enable: bool = False) -> None: handler = getattr(self, f"async_preset_handler_{preset}") await handler(enable) - def to_json(self) -> dict: - """Return a JSON representation of the thermostat.""" - json = super().to_json() - json["hvac_modes"] = self.hvac_modes - json["fan_modes"] = self.fan_modes - json["preset_modes"] = self.preset_modes - json["supported_features"] = self.supported_features - json["max_temp"] = self.max_temp - json["min_temp"] = self.min_temp - return json - - def get_state(self) -> dict: - """Get the state of the lock.""" - response = super().get_state() - response["current_temperature"] = self.current_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 - response.update(self.extra_state_attributes) - return response - @MULTI_MATCH( cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT, "sinope_manufacturer_specific"}, @@ -610,7 +626,7 @@ def __init__( ] self._supported_flags |= ClimateEntityFeature.PRESET_MODE - @property + @functools.cached_property def hvac_modes(self) -> list[HVACMode]: """Return only the heat mode, because the device can't be turned off.""" return [HVACMode.HEAT] @@ -698,7 +714,7 @@ def __init__( ] self._supported_flags |= ClimateEntityFeature.PRESET_MODE - @property + @functools.cached_property def hvac_modes(self) -> list[HVACMode]: """Return only the heat mode, because the device can't be turned off.""" return [HVACMode.HEAT] @@ -760,7 +776,7 @@ async def async_preset_handler(self, preset: str, enable: bool = False) -> None: class StelproFanHeater(Thermostat): """Stelpro Fan Heater implementation.""" - @property + @functools.cached_property def hvac_modes(self) -> list[HVACMode]: """Return only the heat mode, because the device can't be turned off.""" return [HVACMode.HEAT] diff --git a/zha/application/platforms/cover/__init__.py b/zha/application/platforms/cover/__init__.py index 0aa415f..8c4f55b 100644 --- a/zha/application/platforms/cover/__init__.py +++ b/zha/application/platforms/cover/__init__.py @@ -83,12 +83,67 @@ def __init__( ) self._target_lift_position: int | None = None self._target_tilt_position: int | None = None + self._state: str self._determine_initial_state() self._cover_cluster_handler.on_event( CLUSTER_HANDLER_ATTRIBUTE_UPDATED, self.handle_cluster_handler_attribute_updated, ) + @property + def state(self) -> dict[str, Any]: + """Get the state of the cover.""" + response = super().state + response.update( + { + ATTR_CURRENT_POSITION: self.current_cover_position, + "state": self._state, + "is_opening": self.is_opening, + "is_closing": self.is_closing, + "is_closed": self.is_closed, + } + ) + return response + + @property + def is_closed(self) -> bool | None: + """Return True if the cover is closed. + + In HA None is unknown, 0 is closed, 100 is fully open. + In ZCL 0 is fully open, 100 is fully closed. + Keep in mind the values have already been flipped to match HA + in the WindowCovering cluster handler + """ + if self.current_cover_position is None: + return None + return self.current_cover_position == 0 + + @property + def is_opening(self) -> bool: + """Return if the cover is opening or not.""" + return self._state == STATE_OPENING + + @property + def is_closing(self) -> bool: + """Return if the cover is closing or not.""" + return self._state == STATE_CLOSING + + @property + def current_cover_position(self) -> int | None: + """Return the current position of ZHA cover. + + In HA None is unknown, 0 is closed, 100 is fully open. + In ZCL 0 is fully open, 100 is fully closed. + Keep in mind the values have already been flipped to match HA + in the WindowCovering cluster handler + """ + return self._cover_cluster_handler.current_position_lift_percentage + + @property + def current_cover_tilt_position(self) -> int | None: + """Return the current tilt position of the cover.""" + return self._cover_cluster_handler.current_position_tilt_percentage + def _determine_supported_features(self) -> CoverEntityFeature: """Determine the supported cover features.""" supported_features: CoverEntityFeature = ( @@ -168,45 +223,6 @@ def _determine_state(self, position_or_tilt, is_lift_update=True) -> None: return self._state = STATE_OPEN - @property - def is_closed(self) -> bool | None: - """Return True if the cover is closed. - - In HA None is unknown, 0 is closed, 100 is fully open. - In ZCL 0 is fully open, 100 is fully closed. - Keep in mind the values have already been flipped to match HA - in the WindowCovering cluster handler - """ - if self.current_cover_position is None: - return None - return self.current_cover_position == 0 - - @property - def is_opening(self) -> bool: - """Return if the cover is opening or not.""" - return self._state == STATE_OPENING - - @property - def is_closing(self) -> bool: - """Return if the cover is closing or not.""" - return self._state == STATE_CLOSING - - @property - def current_cover_position(self) -> int | None: - """Return the current position of ZHA cover. - - In HA None is unknown, 0 is closed, 100 is fully open. - In ZCL 0 is fully open, 100 is fully closed. - Keep in mind the values have already been flipped to match HA - in the WindowCovering cluster handler - """ - return self._cover_cluster_handler.current_position_lift_percentage - - @property - def current_cover_tilt_position(self) -> int | None: - """Return the current tilt position of the cover.""" - return self._cover_cluster_handler.current_position_tilt_percentage - def handle_cluster_handler_attribute_updated( self, event: ClusterAttributeUpdatedEvent ) -> None: @@ -315,20 +331,6 @@ async def async_stop_cover_tilt(self, **kwargs: Any) -> None: # pylint: disable self._determine_state(self.current_cover_tilt_position, is_lift_update=False) self.maybe_emit_state_changed_event() - def get_state(self) -> dict: - """Get the state of the cover.""" - response = super().get_state() - response.update( - { - ATTR_CURRENT_POSITION: self.current_cover_position, - "state": self._state, - "is_opening": self.is_opening, - "is_closing": self.is_closing, - "is_closed": self.is_closed, - } - ) - return response - @MULTI_MATCH( cluster_handler_names={ @@ -375,6 +377,23 @@ def __init__( CLUSTER_HANDLER_ATTRIBUTE_UPDATED, self.handle_cluster_handler_set_level ) + @property + def state(self) -> dict[str, Any]: + """Get the state of the cover.""" + if (closed := self.is_closed) is None: + state = None + else: + state = STATE_CLOSED if closed else STATE_OPEN + response = super().state + response.update( + { + ATTR_CURRENT_POSITION: self.current_cover_position, + "is_closed": self.is_closed, + "state": state, + } + ) + return response + @property def current_cover_position(self) -> int | None: """Return current position of cover. @@ -444,22 +463,6 @@ async def async_stop_cover(self, **kwargs: Any) -> None: # pylint: disable=unus if res[1] != Status.SUCCESS: raise ZHAException(f"Failed to stop cover: {res[1]}") - def get_state(self) -> dict: - """Get the state of the cover.""" - if (closed := self.is_closed) is None: - state = None - else: - state = STATE_CLOSED if closed else STATE_OPEN - response = super().get_state() - response.update( - { - ATTR_CURRENT_POSITION: self.current_cover_position, - "is_closed": self.is_closed, - "state": state, - } - ) - return response - @MULTI_MATCH( cluster_handler_names={CLUSTER_HANDLER_LEVEL, CLUSTER_HANDLER_ON_OFF}, diff --git a/zha/application/platforms/device_tracker.py b/zha/application/platforms/device_tracker.py index 5f2f5dd..326a5de 100644 --- a/zha/application/platforms/device_tracker.py +++ b/zha/application/platforms/device_tracker.py @@ -5,7 +5,7 @@ from enum import StrEnum import functools import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from zigpy.zcl.clusters.general import PowerConfiguration @@ -83,6 +83,41 @@ def __init__( getattr(self, "__polling_interval"), ) + @property + def state(self) -> dict[str, Any]: + """Return the state of the device.""" + response = super().state + response.update( + { + "connected": self._connected, + "battery_level": self._battery_level, + } + ) + return response + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._connected + + @functools.cached_property + def source_type(self) -> SourceType: + """Return the source type, eg gps or router, of the device.""" + return SourceType.ROUTER + + @property + def battery_level(self): + """Return the battery level of the device. + + Percentage from 0-100. + """ + return self._battery_level + + @periodic((30, 45)) + async def _refresh(self) -> None: + """Refresh the state of the device tracker.""" + await self.async_update() + async def async_update(self) -> None: """Handle polling.""" if self.device.last_seen is None: @@ -95,21 +130,6 @@ async def async_update(self) -> None: self._connected = True self.maybe_emit_state_changed_event() - @periodic((30, 45)) - async def _refresh(self) -> None: - """Refresh the state of the device tracker.""" - await self.async_update() - - @property - def is_connected(self): - """Return true if the device is connected to the network.""" - return self._connected - - @property - def source_type(self) -> SourceType: - """Return the source type, eg gps or router, of the device.""" - return SourceType.ROUTER - def handle_cluster_handler_attribute_updated( self, event: ClusterAttributeUpdatedEvent ) -> None: @@ -123,22 +143,3 @@ def handle_cluster_handler_attribute_updated( self._connected = True self._battery_level = Battery.formatter(event.attribute_value) self.maybe_emit_state_changed_event() - - @property - def battery_level(self): - """Return the battery level of the device. - - Percentage from 0-100. - """ - return self._battery_level - - def get_state(self) -> dict: - """Return the state of the device.""" - response = super().get_state() - response.update( - { - "connected": self._connected, - "battery_level": self._battery_level, - } - ) - return response diff --git a/zha/application/platforms/fan/__init__.py b/zha/application/platforms/fan/__init__.py index c88e1f8..5bda365 100644 --- a/zha/application/platforms/fan/__init__.py +++ b/zha/application/platforms/fan/__init__.py @@ -3,14 +3,22 @@ from __future__ import annotations from abc import abstractmethod +import dataclasses +from dataclasses import dataclass import functools import math from typing import TYPE_CHECKING, Any +from zigpy.types import EUI64 from zigpy.zcl.clusters import hvac from zha.application import Platform -from zha.application.platforms import BaseEntity, GroupEntity, PlatformEntity +from zha.application.platforms import ( + BaseEntity, + BaseEntityInfo, + GroupEntity, + PlatformEntity, +) from zha.application.platforms.fan.const import ( ATTR_PERCENTAGE, ATTR_PRESET_MODE, @@ -36,6 +44,7 @@ from zha.application.registries import PLATFORM_ENTITIES from zha.zigbee.cluster_handlers import ( ClusterAttributeUpdatedEvent, + ClusterHandlerInfo, wrap_zigpy_exceptions, ) from zha.zigbee.cluster_handlers.const import ( @@ -54,6 +63,28 @@ MULTI_MATCH = functools.partial(PLATFORM_ENTITIES.multipass_match, Platform.FAN) +@dataclass(frozen=True, kw_only=True) +class FanEntityInfo(BaseEntityInfo): + """Fan entity info.""" + + # combination of PlatformEntityInfo and GroupEntityInfo + unique_id: str + platform: str + class_name: str + + cluster_handlers: list[ClusterHandlerInfo] | None = dataclasses.field(default=None) + device_ieee: EUI64 | None = dataclasses.field(default=None) + endpoint_id: int | None = dataclasses.field(default=None) + group_id: int | None = dataclasses.field(default=None) + name: str | None = dataclasses.field(default=None) + available: bool | None = dataclasses.field(default=None) + + preset_modes: list[str] + supported_features: int + speed_count: int + speed_list: list[str] + + class BaseFan(BaseEntity): """Base representation of a ZHA fan.""" @@ -62,37 +93,37 @@ class BaseFan(BaseEntity): _attr_supported_features = FanEntityFeature.SET_SPEED _attr_translation_key: str = "fan" - @property + @functools.cached_property def preset_modes(self) -> list[str]: """Return the available preset modes.""" return list(self.preset_modes_to_name.values()) - @property + @functools.cached_property def preset_modes_to_name(self) -> dict[int, str]: """Return a dict from preset mode to name.""" return PRESET_MODES_TO_NAME - @property + @functools.cached_property def preset_name_to_mode(self) -> dict[str, int]: """Return a dict from preset name to mode.""" return {v: k for k, v in self.preset_modes_to_name.items()} - @property + @functools.cached_property def default_on_percentage(self) -> int: """Return the default on percentage.""" return DEFAULT_ON_PERCENTAGE - @property + @functools.cached_property def speed_range(self) -> tuple[int, int]: """Return the range of speeds the fan supports. Off is not included.""" return SPEED_RANGE - @property + @functools.cached_property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" return int_states_in_range(self.speed_range) - @property + @functools.cached_property def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_SET_SPEED @@ -102,12 +133,12 @@ def is_on(self) -> bool: """Return true if the entity is on.""" return self.speed not in [SPEED_OFF, None] # pylint: disable=no-member - @property + @functools.cached_property def percentage_step(self) -> float: """Return the step size for percentage.""" return 100 / self.speed_count - @property + @functools.cached_property def speed_list(self) -> list[str]: """Get the list of available speeds.""" speeds = [SPEED_OFF, *LEGACY_SPEED_LIST] @@ -177,17 +208,6 @@ def percentage_to_speed(self, percentage: int) -> str: return SPEED_OFF return percentage_to_ordered_list_item(LEGACY_SPEED_LIST, percentage) - def to_json(self) -> dict: - """Return a JSON representation of the binary sensor.""" - json = super().to_json() - json["preset_modes"] = self.preset_modes - json["supported_features"] = self.supported_features - json["speed_count"] = self.speed_count - json["speed_list"] = self.speed_list - json["percentage_step"] = self.percentage_step - json["default_on_percentage"] = self.default_on_percentage - return json - @STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_FAN) class Fan(PlatformEntity, BaseFan): @@ -212,6 +232,31 @@ def __init__( self.handle_cluster_handler_attribute_updated, ) + @functools.cached_property + def info_object(self) -> FanEntityInfo: + """Return a representation of the binary sensor.""" + return FanEntityInfo( + **super().info_object.__dict__, + preset_modes=self.preset_modes, + supported_features=self.supported_features, + speed_count=self.speed_count, + speed_list=self.speed_list, + ) + + @property + def state(self) -> dict: + """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 + @property def percentage(self) -> int | None: """Return the current speed percentage.""" @@ -240,19 +285,6 @@ def speed(self) -> str | None: return None return self.percentage_to_speed(percentage) - def get_state(self) -> dict: - """Return the state of the fan.""" - response = super().get_state() - response.update( - { - "preset_mode": self.preset_mode, - "percentage": self.percentage, - "is_on": self.is_on, - "speed": self.speed, - } - ) - return response - async def _async_set_fan_mode(self, fan_mode: int) -> None: """Set the fan mode for the fan.""" await self._fan_cluster_handler.async_set_speed(fan_mode) @@ -272,6 +304,34 @@ def __init__(self, group: Group): self._available: bool = False self._percentage = None self._preset_mode = None + if hasattr(self, "info_object"): + delattr(self, "info_object") + self.update() + + @functools.cached_property + def info_object(self) -> FanEntityInfo: + """Return a representation of the binary sensor.""" + return FanEntityInfo( + **super().info_object.__dict__, + preset_modes=self.preset_modes, + supported_features=self.supported_features, + speed_count=self.speed_count, + speed_list=self.speed_list, + ) + + @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 @property def percentage(self) -> int | None: @@ -292,19 +352,6 @@ def speed(self) -> str | None: return None return self.percentage_to_speed(percentage) - def get_state(self) -> dict: - """Return the state of the fan.""" - response = super().get_state() - response.update( - { - "preset_mode": self.preset_mode, - "percentage": self.percentage, - "is_on": self.is_on, - "speed": self.speed, - } - ) - return response - async def _async_set_fan_mode(self, fan_mode: int) -> None: """Set the fan mode for the group.""" @@ -317,8 +364,7 @@ def update(self, _: Any = None) -> None: """Attempt to retrieve on off state from the fan.""" self.debug("Updating fan group entity state") platform_entities = self._group.get_platform_entities(self.PLATFORM) - all_entities = [entity.to_json() for entity in platform_entities] - all_states = [entity["state"] for entity in all_entities] + all_states = [entity.state for entity in platform_entities] self.debug( "All platform entity states for group entity members: %s", all_states ) @@ -384,12 +430,12 @@ def __init__( self.handle_cluster_handler_attribute_updated, ) - @property + @functools.cached_property def preset_modes_to_name(self) -> dict[int, str]: """Return a dict from preset mode to name.""" return IKEA_PRESET_MODES_TO_NAME - @property + @functools.cached_property def speed_range(self) -> tuple[int, int]: """Return the range of speeds the fan supports. Off is not included.""" return IKEA_SPEED_RANGE @@ -411,12 +457,12 @@ class KofFan(Fan): _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE - @property + @functools.cached_property def speed_range(self) -> tuple[int, int]: """Return the range of speeds the fan supports. Off is not included.""" return (1, 4) - @property + @functools.cached_property def preset_modes_to_name(self) -> dict[int, str]: """Return a dict from preset mode to name.""" return {6: PRESET_MODE_SMART} diff --git a/zha/application/platforms/light/__init__.py b/zha/application/platforms/light/__init__.py index 162895f..9263751 100644 --- a/zha/application/platforms/light/__init__.py +++ b/zha/application/platforms/light/__init__.py @@ -8,11 +8,14 @@ import asyncio from collections import Counter from collections.abc import Callable +import dataclasses +from dataclasses import dataclass import functools import itertools import logging from typing import TYPE_CHECKING, Any +from zigpy.types import EUI64 from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.foundation import Status @@ -27,7 +30,12 @@ ZHA_OPTIONS, ) from zha.application.helpers import async_get_zha_config_value -from zha.application.platforms import BaseEntity, GroupEntity, PlatformEntity +from zha.application.platforms import ( + BaseEntity, + BaseEntityInfo, + GroupEntity, + PlatformEntity, +) from zha.application.platforms.helpers import ( find_state_attributes, mean_tuple, @@ -67,7 +75,7 @@ from zha.application.registries import PLATFORM_ENTITIES from zha.debounce import Debouncer from zha.decorators import periodic -from zha.zigbee.cluster_handlers import ClusterAttributeUpdatedEvent +from zha.zigbee.cluster_handlers import ClusterAttributeUpdatedEvent, ClusterHandlerInfo from zha.zigbee.cluster_handlers.const import ( CLUSTER_HANDLER_ATTRIBUTE_UPDATED, CLUSTER_HANDLER_COLOR, @@ -88,6 +96,28 @@ GROUP_MATCH = functools.partial(PLATFORM_ENTITIES.group_match, Platform.LIGHT) +@dataclass(frozen=True, kw_only=True) +class LightEntityInfo(BaseEntityInfo): + """Light entity info.""" + + # combination of PlatformEntityInfo and GroupEntityInfo + unique_id: str + platform: str + class_name: str + + cluster_handlers: list[ClusterHandlerInfo] | None = dataclasses.field(default=None) + device_ieee: EUI64 | None = dataclasses.field(default=None) + endpoint_id: int | None = dataclasses.field(default=None) + group_id: int | None = dataclasses.field(default=None) + name: str | None = dataclasses.field(default=None) + available: bool | None = dataclasses.field(default=None) + + effect_list: list[str] | None = dataclasses.field(default=None) + supported_features: LightEntityFeature + min_mireds: int + max_mireds: int + + class BaseLight(BaseEntity, ABC): """Operations common to all light entities.""" @@ -126,9 +156,10 @@ def __init__(self, *args, **kwargs): self._transitioning_group: bool = False self._transition_listener: Callable[[], None] | None = None - def get_state(self) -> dict[str, Any]: + @property + def state(self) -> dict[str, Any]: """Return the state of the light.""" - response = super().get_state() + response = super().state response["on"] = self.is_on response["brightness"] = self.brightness response["hs_color"] = self.hs_color @@ -142,45 +173,6 @@ def get_state(self) -> dict[str, Any]: response["supported_color_modes"] = self._supported_color_modes return response - @property - def is_on(self) -> bool: - """Return true if entity is on.""" - if self._state is None: - return False - return self._state - - @property - def brightness(self) -> int | None: - """Return the brightness of this light.""" - return self._brightness - - @property - def min_mireds(self) -> int | None: - """Return the coldest color_temp that this light supports.""" - return self._min_mireds - - @property - def max_mireds(self) -> int | None: - """Return the warmest color_temp that this light supports.""" - return self._max_mireds - - def handle_cluster_handler_set_level(self, event: LevelChangeEvent) -> None: - """Set the brightness of this light between 0..254. - - brightness level 255 is a special value instructing the device to come - on at `on_level` Zigbee attribute value, regardless of the last set - level - """ - if self.is_transitioning: - self.debug( - "received level change event %s while transitioning - skipping update", - event, - ) - return - value = max(0, min(254, event.level)) - self._brightness = value - self.maybe_emit_state_changed_event() - @property def hs_color(self) -> tuple[float, float] | None: """Return the hs color value [int, int].""" @@ -221,14 +213,44 @@ def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return self._supported_color_modes - def to_json(self) -> dict: - """Return a JSON representation of the select.""" - json = super().to_json() - json["supported_features"] = self.supported_features - json["effect_list"] = self.effect_list - json["min_mireds"] = self.min_mireds - json["max_mireds"] = self.max_mireds - return json + @property + def is_on(self) -> bool: + """Return true if entity is on.""" + if self._state is None: + return False + return self._state + + @property + def brightness(self) -> int | None: + """Return the brightness of this light.""" + return self._brightness + + @property + def min_mireds(self) -> int | None: + """Return the coldest color_temp that this light supports.""" + return self._min_mireds + + @property + def max_mireds(self) -> int | None: + """Return the warmest color_temp that this light supports.""" + return self._max_mireds + + def handle_cluster_handler_set_level(self, event: LevelChangeEvent) -> None: + """Set the brightness of this light between 0..254. + + brightness level 255 is a special value instructing the device to come + on at `on_level` Zigbee attribute value, regardless of the last set + level + """ + if self.is_transitioning: + self.debug( + "received level change event %s while transitioning - skipping update", + event, + ) + return + value = max(0, min(254, event.level)) + self._brightness = value + self.maybe_emit_state_changed_event() def set_level(self, value: int) -> None: """Set the brightness of this light between 0..254. @@ -844,6 +866,17 @@ def __init__( getattr(self, "__polling_interval"), ) + @functools.cached_property + def info_object(self) -> LightEntityInfo: + """Return a representation of the select.""" + return LightEntityInfo( + **super().info_object.__dict__, + effect_list=self.effect_list, + supported_features=self.supported_features, + min_mireds=self.min_mireds, + max_mireds=self.max_mireds, + ) + @periodic(_REFRESH_INTERVAL) async def _refresh(self) -> None: """Call async_get_state at an interval.""" @@ -1176,8 +1209,21 @@ def __init__(self, group: Group): function=self._force_member_updates, ) self._debounced_member_refresh = force_refresh_debouncer + if hasattr(self, "info_object"): + delattr(self, "info_object") self.update() + @functools.cached_property + def info_object(self) -> LightEntityInfo: + """Return a representation of the select.""" + return LightEntityInfo( + **super().info_object.__dict__, + effect_list=self.effect_list, + supported_features=self.supported_features, + min_mireds=self.min_mireds, + max_mireds=self.max_mireds, + ) + # remove this when all ZHA platforms and base entities are updated @property def available(self) -> bool: @@ -1218,7 +1264,7 @@ def update(self, _: Any = None) -> None: """Query all members and determine the light group state.""" self.debug("Updating light group entity state") platform_entities = self._group.get_platform_entities(self.PLATFORM) - all_states = [entity.get_state() for entity in platform_entities] + all_states = [entity.state for entity in platform_entities] states: list = list(filter(None, all_states)) self.debug( "All platform entity states for group entity members: %s", all_states diff --git a/zha/application/platforms/lock/__init__.py b/zha/application/platforms/lock/__init__.py index ed6c701..761e4f1 100644 --- a/zha/application/platforms/lock/__init__.py +++ b/zha/application/platforms/lock/__init__.py @@ -50,7 +50,7 @@ def __init__( self._doorlock_cluster_handler: ClusterHandler = self.cluster_handlers.get( CLUSTER_HANDLER_DOORLOCK ) - self._state = VALUE_TO_STATE.get( + self._state: str | None = VALUE_TO_STATE.get( self._doorlock_cluster_handler.cluster.get("lock_state"), None ) self._doorlock_cluster_handler.on_event( @@ -58,6 +58,13 @@ def __init__( self.handle_cluster_handler_attribute_updated, ) + @property + def state(self) -> dict[str, Any]: + """Get the state of the lock.""" + response = super().state + response["is_locked"] = self.is_locked + return response + @property def is_locked(self) -> bool: """Return true if entity is locked.""" @@ -117,9 +124,3 @@ def handle_cluster_handler_attribute_updated( return self._state = VALUE_TO_STATE.get(event.attribute_value, self._state) self.maybe_emit_state_changed_event() - - def get_state(self) -> dict: - """Get the state of the lock.""" - response = super().get_state() - response["is_locked"] = self.is_locked - return response diff --git a/zha/application/platforms/number/__init__.py b/zha/application/platforms/number/__init__.py index c71d00b..e68f3ad 100644 --- a/zha/application/platforms/number/__init__.py +++ b/zha/application/platforms/number/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import functools import logging from typing import TYPE_CHECKING, Any, Self @@ -11,7 +12,7 @@ from zha.application import Platform from zha.application.const import ENTITY_METADATA -from zha.application.platforms import EntityCategory, PlatformEntity +from zha.application.platforms import EntityCategory, PlatformEntity, PlatformEntityInfo from zha.application.platforms.helpers import validate_device_class from zha.application.platforms.number.const import ( ICONS, @@ -46,6 +47,28 @@ ) +@dataclass(frozen=True, kw_only=True) +class NumberEntityInfo(PlatformEntityInfo): + """Number entity info.""" + + engineering_units: int + application_type: int + min_value: float | None + max_value: float | None + step: float | None + + +@dataclass(frozen=True, kw_only=True) +class NumberConfigurationEntityInfo(PlatformEntityInfo): + """Number configuration entity info.""" + + min_value: float | None + max_value: float | None + step: float | None + multiplier: float | None + device_class: str | None + + @STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ANALOG_OUTPUT) class Number(PlatformEntity): """Representation of a ZHA Number entity.""" @@ -72,6 +95,25 @@ def __init__( self.handle_cluster_handler_attribute_updated, ) + @functools.cached_property + def info_object(self) -> NumberEntityInfo: + """Return a representation of the number entity.""" + return NumberEntityInfo( + **super().info_object.__dict__, + engineering_units=self._analog_output_cluster_handler.engineering_units, + application_type=self._analog_output_cluster_handler.application_type, + min_value=self.native_min_value, + max_value=self.native_max_value, + step=self.native_step, + ) + + @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.""" @@ -98,7 +140,7 @@ def native_step(self) -> float | None: """Return the value step.""" return self._analog_output_cluster_handler.resolution - @property + @functools.cached_property def name(self) -> str | None: """Return the name of the number entity.""" description = self._analog_output_cluster_handler.description @@ -106,7 +148,7 @@ def name(self) -> str | None: return f"{super().name} {description}" return super().name - @property + @functools.cached_property def icon(self) -> str | None: """Return the icon to be used for this entity.""" application_type = self._analog_output_cluster_handler.application_type @@ -114,13 +156,13 @@ def icon(self) -> str | None: return ICONS.get(application_type >> 16, None) return None - @property + @functools.cached_property def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" engineering_units = self._analog_output_cluster_handler.engineering_units return UNITS.get(engineering_units) - @property + @functools.cached_property def mode(self) -> NumberMode: """Return the mode of the entity.""" return self._attr_mode @@ -143,23 +185,6 @@ async def async_set_value(self, value: Any, **kwargs: Any) -> None: # pylint: d if await self._analog_output_cluster_handler.async_set_present_value(num_value): self.maybe_emit_state_changed_event() - def to_json(self) -> dict: - """Return the JSON representation of the number entity.""" - json = super().to_json() - json["engineer_units"] = self._analog_output_cluster_handler.engineering_units - json["application_type"] = self._analog_output_cluster_handler.application_type - json["step"] = self.native_step - json["min_value"] = self.native_min_value - json["max_value"] = self.native_max_value - json["name"] = self.name - return json - - def get_state(self) -> dict: - """Return the state of the entity.""" - response = super().get_state() - response["state"] = self.native_value - return response - class NumberConfigurationEntity(PlatformEntity): """Representation of a ZHA number configuration entity.""" @@ -247,6 +272,25 @@ def _init_from_quirks_metadata(self, entity_metadata: NumberMetadata) -> None: self.handle_cluster_handler_attribute_updated, ) + @functools.cached_property + def info_object(self) -> NumberConfigurationEntityInfo: + """Return a representation of the number entity.""" + return NumberConfigurationEntityInfo( + **super().info_object.__dict__, + min_value=self._attr_native_min_value, + max_value=self._attr_native_max_value, + step=self._attr_native_step, + multiplier=self._attr_multiplier, + device_class=self._attr_device_class, + ) + + @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: """Return the current value.""" @@ -270,19 +314,19 @@ def native_step(self) -> float | None: """Return the value step.""" return self._attr_native_step - @property + @functools.cached_property def icon(self) -> str | None: """Return the icon to be used for this entity.""" return self._attr_icon - @property + @functools.cached_property def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if hasattr(self, "_attr_native_unit_of_measurement"): return self._attr_native_unit_of_measurement return None - @property + @functools.cached_property def mode(self) -> NumberMode: """Return the mode of the entity.""" return self._attr_mode @@ -312,23 +356,6 @@ def handle_cluster_handler_attribute_updated( if event.attribute_name == self._attribute_name: self.maybe_emit_state_changed_event() - def to_json(self) -> dict: - """Return the JSON representation of the number entity.""" - json = super().to_json() - json["multiplier"] = self._attr_multiplier - json["device_class"] = self._attr_device_class - json["step"] = self._attr_native_step - json["min_value"] = self._attr_native_min_value - json["max_value"] = self._attr_native_max_value - json["name"] = self.name - return json - - def get_state(self) -> dict: - """Return the state of the entity.""" - response = super().get_state() - response["state"] = self.native_value - return response - @CONFIG_DIAGNOSTIC_MATCH( cluster_handler_names="opple_cluster", diff --git a/zha/application/platforms/select.py b/zha/application/platforms/select.py index 7ccc2c3..48159b5 100644 --- a/zha/application/platforms/select.py +++ b/zha/application/platforms/select.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass from enum import Enum import functools import logging @@ -17,7 +18,7 @@ from zha.application import Platform from zha.application.const import ENTITY_METADATA, Strobe -from zha.application.platforms import EntityCategory, PlatformEntity +from zha.application.platforms import EntityCategory, PlatformEntity, PlatformEntityInfo from zha.application.registries import PLATFORM_ENTITIES from zha.zigbee.cluster_handlers import ClusterAttributeUpdatedEvent from zha.zigbee.cluster_handlers.const import ( @@ -41,6 +42,14 @@ _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class EnumSelectInfo(PlatformEntityInfo): + """Enum select entity info.""" + + enum: str + options: list[str] + + class EnumSelectEntity(PlatformEntity): """Representation of a ZHA select entity.""" @@ -63,6 +72,22 @@ def __init__( self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] super().__init__(unique_id, cluster_handlers, endpoint, device, **kwargs) + @functools.cached_property + def info_object(self) -> EnumSelectInfo: + """Return a representation of the select.""" + return EnumSelectInfo( + **super().info_object.__dict__, + enum=self._enum.__name__, + options=self._attr_options, + ) + + @property + def state(self) -> dict: + """Return the state of the select.""" + response = super().state + response["state"] = self.current_option + return response + @property def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" @@ -78,19 +103,6 @@ async def async_select_option(self, option: str) -> None: ] self.maybe_emit_state_changed_event() - def to_json(self) -> dict: - """Return a JSON representation of the select.""" - json = super().to_json() - json["enum"] = self._enum.__name__ - json["options"] = self._attr_options - return json - - def get_state(self) -> dict: - """Return the state of the select.""" - response = super().get_state() - response["state"] = self.current_option - return response - class NonZCLSelectEntity(EnumSelectEntity): """Representation of a ZHA select entity with no ZCL interaction.""" @@ -198,6 +210,22 @@ def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: self._attribute_name = entity_metadata.attribute_name self._enum = entity_metadata.enum + @functools.cached_property + def info_object(self) -> EnumSelectInfo: + """Return a representation of the select.""" + return EnumSelectInfo( + **super().info_object.__dict__, + enum=self._enum.__name__, + options=self._attr_options, + ) + + @property + def state(self) -> dict[str, Any]: + """Return the state of the select.""" + response = super().state + response["state"] = self.current_option + return response + @property def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" @@ -222,19 +250,6 @@ def handle_cluster_handler_attribute_updated( if event.attribute_name == self._attribute_name: self.maybe_emit_state_changed_event() - def to_json(self) -> dict: - """Return a JSON representation of the select.""" - json = super().to_json() - json["enum"] = self._enum.__name__ - json["options"] = self._attr_options - return json - - def get_state(self) -> dict: - """Return the state of the select.""" - response = super().get_state() - response["state"] = self.current_option - return response - @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF) class StartupOnOffSelectEntity(ZCLEnumSelectEntity): diff --git a/zha/application/platforms/sensor/__init__.py b/zha/application/platforms/sensor/__init__.py index 79c0ad3..49eb33b 100644 --- a/zha/application/platforms/sensor/__init__.py +++ b/zha/application/platforms/sensor/__init__.py @@ -18,7 +18,14 @@ from zha.application import Platform from zha.application.const import ENTITY_METADATA -from zha.application.platforms import BaseEntity, EntityCategory, PlatformEntity +from zha.application.platforms import ( + BaseEntity, + BaseEntityInfo, + BaseIdentifiers, + EntityCategory, + PlatformEntity, + PlatformEntityInfo, +) from zha.application.platforms.climate.const import HVACAction from zha.application.platforms.helpers import validate_device_class from zha.application.platforms.sensor.const import SensorDeviceClass, SensorStateClass @@ -98,6 +105,39 @@ ) +@dataclass(frozen=True, kw_only=True) +class SensorEntityInfo(PlatformEntityInfo): + """Sensor entity info.""" + + attribute: str + decimals: int + divisor: int + multiplier: int + 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.""" + + name: str + device_ieee: str + available: bool + counter: str + counter_value: int + counter_groups: str + counter_group: str + + +@dataclass(frozen=True, kw_only=True) +class DeviceCounterSensorIdentifiers(BaseIdentifiers): + """Device counter sensor identifiers.""" + + device_ieee: str + + class Sensor(PlatformEntity): """Base ZHA sensor.""" @@ -175,6 +215,28 @@ def _init_from_quirks_metadata(self, entity_metadata: ZCLSensorMetadata) -> None entity_metadata.unit ).value + @functools.cached_property + def info_object(self) -> SensorEntityInfo: + """Return a representation of the sensor.""" + return SensorEntityInfo( + **super().info_object.__dict__, + attribute=self._attribute_name, + decimals=self._decimals, + divisor=self._divisor, + multiplier=self._multiplier, + unit=self._attr_native_unit_of_measurement, + device_class=self._attr_device_class, + state_class=self._attr_state_class, + ) + + @property + def state(self) -> dict: + """Return the state for this sensor.""" + response = super().state + native_value = self.native_value + response["state"] = native_value + return response + @property def native_value(self) -> str | int | float | None: """Return the state of the entity.""" @@ -184,13 +246,6 @@ def native_value(self) -> str | int | float | None: return None return self.formatter(raw_state) - def get_state(self) -> dict: - """Return the state for this sensor.""" - response = super().get_state() - native_value = self.native_value - response["state"] = native_value - return response - def handle_cluster_handler_attribute_updated( self, event: ClusterAttributeUpdatedEvent, # pylint: disable=unused-argument @@ -199,18 +254,6 @@ def handle_cluster_handler_attribute_updated( if event.attribute_name == self._attribute_name: self.maybe_emit_state_changed_event() - def to_json(self) -> dict: - """Return a JSON representation of the sensor.""" - json = super().to_json() - json["attribute"] = self._attribute_name - json["decimals"] = self._decimals - json["divisor"] = self._divisor - json["multiplier"] = self._multiplier - json["unit"] = self._attr_native_unit_of_measurement - json["device_class"] = self._attr_device_class - json["state_class"] = self._attr_state_class - return json - def formatter(self, value: int | enum.IntEnum) -> int | float | str | None: """Numeric pass-through formatter.""" if self._decimals > 0: @@ -337,6 +380,34 @@ def __init__( if self.unique_id not in self._device.platform_entities: self._device.platform_entities[self.unique_id] = self + @functools.cached_property + def identifiers(self) -> DeviceCounterSensorIdentifiers: + """Return a dict with the information necessary to identify this entity.""" + return 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__, + name=self._attr_name, + device_ieee=str(self._device.ieee), + available=self.available, + counter=self._zigpy_counter.name, + counter_value=self._zigpy_counter.value, + counter_groups=self._zigpy_counter_groups, + 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 name(self) -> str: """Return the name of the platform entity.""" @@ -352,7 +423,7 @@ def available(self) -> bool: """Return entity availability.""" return self._device.available - @property + @functools.cached_property def device(self) -> Device: """Return the device.""" return self._device @@ -374,32 +445,6 @@ async def _refresh(self): self._device.gateway.config.allow_polling, ) - def get_identifiers(self) -> dict[str, str | int]: - """Return a dict with the information necessary to identify this entity.""" - return { - "unique_id": self.unique_id, - "platform": self.PLATFORM, - "device_ieee": self._device.ieee, - } - - def get_state(self) -> dict[str, Any]: - """Return the state for this sensor.""" - response = super().get_state() - response["state"] = self._zigpy_counter.value - return response - - def to_json(self) -> dict: - """Return a JSON representation of the platform entity.""" - json = super().to_json() - json["name"] = self._attr_name - json["device_ieee"] = str(self._device.ieee) - json["available"] = self.available - json["counter"] = self._zigpy_counter.name - json["counter_value"] = self._zigpy_counter.value - json["counter_groups"] = self._zigpy_counter_groups - json["counter_group"] = self._zigpy_counter_group - return json - class EnumSensor(Sensor): """Sensor with value from enum.""" @@ -481,9 +526,10 @@ def formatter(value: int) -> int | None: # pylint: disable=arguments-differ value = round(value / 2) return value - def get_state(self) -> dict[str, Any]: + @property + def state(self) -> dict[str, Any]: """Return the state for battery sensors.""" - response = super().get_state() + response = super().state battery_size = self._cluster_handler.cluster.get("battery_size") if battery_size is not None: response["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") @@ -511,9 +557,10 @@ class ElectricalMeasurement(PollableSensor): _attr_native_unit_of_measurement: str = UnitOfPower.WATT _div_mul_prefix: str | None = "ac_power" - def get_state(self) -> dict[str, Any]: + @property + def state(self) -> dict[str, Any]: """Return the state for this sensor.""" - response = super().get_state() + response = super().state if self._cluster_handler.measurement_type is not None: response["measurement_type"] = self._cluster_handler.measurement_type @@ -772,13 +819,10 @@ def __init__( self._attr_device_class = entity_description.device_class self._attr_state_class = entity_description.state_class - def formatter(self, value: int) -> int | float: - """Pass through cluster handler formatter.""" - return self._cluster_handler.demand_formatter(value) - - def get_state(self) -> dict[str, Any]: + @property + def state(self) -> dict[str, Any]: """Return state for this sensor.""" - response = super().get_state() + response = super().state if self._cluster_handler.device_type is not None: response["device_type"] = self._cluster_handler.device_type if (status := self._cluster_handler.metering_status) is not None: @@ -791,6 +835,10 @@ def get_state(self) -> dict[str, Any]: response["zcl_unit_of_measurement"] = self._cluster_handler.unit_of_measurement return response + def formatter(self, value: int) -> int | float: + """Pass through cluster handler formatter.""" + return self._cluster_handler.demand_formatter(value) + @dataclass(frozen=True, kw_only=True) class SmartEnergySummationEntityDescription(SmartEnergyMeteringEntityDescription): @@ -1146,6 +1194,19 @@ def create_platform_entity( return cls(unique_id, cluster_handlers, endpoint, device, **kwargs) + @property + def state(self) -> dict: + """Return the current HVAC action.""" + response = super().state + if ( + self._cluster_handler.pi_heating_demand is None + and self._cluster_handler.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.""" @@ -1208,18 +1269,6 @@ def _pi_demand_action(self) -> HVACAction: return HVACAction.IDLE return HVACAction.OFF - def get_state(self) -> dict: - """Return the current HVAC action.""" - response = super().get_state() - if ( - self._cluster_handler.pi_heating_demand is None - and self._cluster_handler.pi_cooling_demand is None - ): - response["state"] = self._rm_rs_action - else: - response["state"] = self._pi_demand_action - return response - @MULTI_MATCH( cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT}, @@ -1286,16 +1335,17 @@ def create_platform_entity( return cls(unique_id, cluster_handlers, endpoint, device, **kwargs) @property - def native_value(self) -> str | int | float | None: - """Return the state of the entity.""" - return getattr(self._device.device, self._unique_id_suffix) - - def get_state(self) -> dict: + def state(self) -> dict: """Return the state of the sensor.""" - response = super().get_state() + response = super().state response["state"] = getattr(self.device.device, self._unique_id_suffix) return response + @property + def native_value(self) -> str | int | float | None: + """Return the state of the entity.""" + return getattr(self._device.device, self._unique_id_suffix) + @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC) class LQISensor(RSSISensor): diff --git a/zha/application/platforms/siren.py b/zha/application/platforms/siren.py index b6e03a4..463f5de 100644 --- a/zha/application/platforms/siren.py +++ b/zha/application/platforms/siren.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from enum import IntFlag import functools from typing import TYPE_CHECKING, Any, Final, cast @@ -23,7 +24,7 @@ WARNING_DEVICE_STROBE_NO, Strobe, ) -from zha.application.platforms import PlatformEntity +from zha.application.platforms import PlatformEntity, PlatformEntityInfo from zha.application.registries import PLATFORM_ENTITIES from zha.zigbee.cluster_handlers.const import CLUSTER_HANDLER_IAS_WD from zha.zigbee.cluster_handlers.security import IasWdClusterHandler @@ -52,6 +53,14 @@ class SirenEntityFeature(IntFlag): DURATION = 16 +@dataclass(frozen=True, kw_only=True) +class SirenEntityInfo(PlatformEntityInfo): + """Siren entity info.""" + + available_tones: dict[int, str] + supported_features: SirenEntityFeature + + @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD) class Siren(PlatformEntity): """Representation of a ZHA siren.""" @@ -75,7 +84,7 @@ def __init__( | SirenEntityFeature.VOLUME_SET | SirenEntityFeature.TONES ) - self._attr_available_tones: list[int | str] | dict[int, str] | None = { + self._attr_available_tones: dict[int, str] = { WARNING_DEVICE_MODE_BURGLAR: "Burglar", WARNING_DEVICE_MODE_FIRE: "Fire", WARNING_DEVICE_MODE_EMERGENCY: "Emergency", @@ -90,6 +99,22 @@ def __init__( self._attr_is_on: bool = False self._off_listener: asyncio.TimerHandle | None = None + @functools.cached_property + def info_object(self) -> SirenEntityInfo: + """Return representation of the siren.""" + return SirenEntityInfo( + **super().info_object.__dict__, + available_tones=self._attr_available_tones, + supported_features=self._attr_supported_features, + ) + + @property + def state(self) -> dict[str, Any]: + """Get the state of the siren.""" + response = super().state + response["state"] = self._attr_is_on + return response + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on siren.""" if self._off_listener: @@ -157,16 +182,3 @@ def async_set_off(self) -> None: self._off_listener.cancel() self._off_listener = None self.maybe_emit_state_changed_event() - - def to_json(self) -> dict: - """Return JSON representation of the siren.""" - json = super().to_json() - json[ATTR_AVAILABLE_TONES] = self._attr_available_tones - json["supported_features"] = self._attr_supported_features - return json - - def get_state(self) -> dict: - """Get the state of the siren.""" - response = super().get_state() - response["state"] = self._attr_is_on - return response diff --git a/zha/application/platforms/switch.py b/zha/application/platforms/switch.py index 7152263..cdadf3e 100644 --- a/zha/application/platforms/switch.py +++ b/zha/application/platforms/switch.py @@ -3,12 +3,15 @@ from __future__ import annotations from abc import ABC +import dataclasses +from dataclasses import dataclass import functools import logging from typing import TYPE_CHECKING, Any, Self, cast from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF from zigpy.quirks.v2 import SwitchMetadata +from zigpy.types import EUI64 from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status @@ -22,7 +25,7 @@ PlatformEntity, ) from zha.application.registries import PLATFORM_ENTITIES -from zha.zigbee.cluster_handlers import ClusterAttributeUpdatedEvent +from zha.zigbee.cluster_handlers import ClusterAttributeUpdatedEvent, ClusterHandlerInfo from zha.zigbee.cluster_handlers.const import ( CLUSTER_HANDLER_ATTRIBUTE_UPDATED, CLUSTER_HANDLER_BASIC, @@ -47,6 +50,29 @@ _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class SwitchConfigurationEntityInfo: + """Switch configuration entity info.""" + + # combination of PlatformEntityInfo and GroupEntityInfo + unique_id: str + platform: str + class_name: str + + cluster_handlers: list[ClusterHandlerInfo] | None = dataclasses.field(default=None) + device_ieee: EUI64 | None = dataclasses.field(default=None) + endpoint_id: int | None = dataclasses.field(default=None) + group_id: int | None = dataclasses.field(default=None) + name: str | None = dataclasses.field(default=None) + available: bool | None = dataclasses.field(default=None) + + attribute_name: str + invert_attribute_name: str | None + force_inverted: bool + off_value: int + on_value: int + + class BaseSwitch(BaseEntity, ABC): """Common base class for zhawss switches.""" @@ -61,6 +87,13 @@ def __init__( self._on_off_cluster_handler: OnOffClusterHandler super().__init__(*args, **kwargs) + @property + def state(self) -> dict[str, Any]: + """Return the state of the switch.""" + response = super().state + response["state"] = self.is_on + return response + @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" @@ -79,12 +112,6 @@ async def async_turn_off(self, **kwargs: Any) -> None: # pylint: disable=unused await self._on_off_cluster_handler.turn_off() self.maybe_emit_state_changed_event() - def get_state(self) -> dict: - """Return the state of the switch.""" - response = super().get_state() - response["state"] = self.is_on - return response - @STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF) class Switch(PlatformEntity, BaseSwitch): @@ -128,6 +155,8 @@ def __init__(self, group: Group): super().__init__(group) self._state: bool self._on_off_cluster_handler = group.zigpy_group.endpoint[OnOff.cluster_id] + if hasattr(self, "info_object"): + delattr(self, "info_object") self.update() @property @@ -155,8 +184,7 @@ 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_entities = [entity.to_json() for entity in platform_entities] - all_states = [entity["state"] for entity in all_entities] + all_states = [entity.state for entity in platform_entities] self.debug( "All platform entity states for group entity members: %s", all_states ) @@ -237,13 +265,25 @@ def _init_from_quirks_metadata(self, entity_metadata: SwitchMetadata) -> None: self._off_value = entity_metadata.off_value self._on_value = entity_metadata.on_value - def handle_cluster_handler_attribute_updated( - self, - event: ClusterAttributeUpdatedEvent, # pylint: disable=unused-argument - ) -> None: - """Handle state update from cluster handler.""" - if event.attribute_name == self._attribute_name: - self.maybe_emit_state_changed_event() + @functools.cached_property + def info_object(self) -> SwitchConfigurationEntityInfo: + """Return representation of the switch configuration entity.""" + return SwitchConfigurationEntityInfo( + **super().info_object.__dict__, + attribute_name=self._attribute_name, + invert_attribute_name=self._inverter_attribute_name, + force_inverted=self._force_inverted, + off_value=self._off_value, + 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: @@ -264,6 +304,14 @@ def is_on(self) -> bool: val = bool(self._cluster_handler.cluster.get(self._attribute_name)) return (not val) if self.inverted else val + def handle_cluster_handler_attribute_updated( + self, + event: ClusterAttributeUpdatedEvent, # pylint: disable=unused-argument + ) -> None: + """Handle state update from cluster handler.""" + if event.attribute_name == self._attribute_name: + self.maybe_emit_state_changed_event() + async def async_turn_on_off(self, state: bool) -> None: """Turn the entity on or off.""" if self.inverted: @@ -301,13 +349,6 @@ async def async_update(self) -> None: self.debug("read values=%s", results) self.maybe_emit_state_changed_event() - def get_state(self) -> dict: - """Return the state of the switch.""" - response = super().get_state() - response["state"] = self.is_on - response["inverted"] = self.inverted - return response - @CONFIG_DIAGNOSTIC_MATCH( cluster_handler_names="tuya_manufacturer", diff --git a/zha/application/platforms/update.py b/zha/application/platforms/update.py index d1f37a1..4de8ff0 100644 --- a/zha/application/platforms/update.py +++ b/zha/application/platforms/update.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass from enum import IntFlag, StrEnum import functools import logging @@ -14,7 +15,7 @@ from zigpy.zcl.foundation import Status from zha.application import Platform -from zha.application.platforms import EntityCategory, PlatformEntity +from zha.application.platforms import EntityCategory, PlatformEntity, PlatformEntityInfo from zha.application.registries import PLATFORM_ENTITIES from zha.decorators import callback from zha.exceptions import ZHAException @@ -99,6 +100,15 @@ class UpdateEntityFeature(IntFlag): ATTR_VERSION: Final = "version" +@dataclass(frozen=True, kw_only=True) +class UpdateEntityInfo(PlatformEntityInfo): + """Update entity info.""" + + supported_features: UpdateEntityFeature + device_class: UpdateDeviceClass + entity_category: EntityCategory + + # old base classes: CoordinatorEntity[ZHAFirmwareUpdateCoordinator], UpdateEntity @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_OTA) class FirmwareUpdateEntity(PlatformEntity): @@ -145,6 +155,24 @@ def __init__( self.handle_cluster_handler_attribute_updated, ) + @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, + device_class=self._attr_device_class, + entity_category=self._attr_entity_category, + ) + + @property + def state(self): + """Get the state for the entity.""" + response = super().state + response["state"] = self._state + response.update(self.state_attributes) + return response + @property def installed_version(self) -> str | None: """Version installed and in use.""" @@ -187,7 +215,7 @@ def supported_features(self) -> UpdateEntityFeature: @property @final - def state(self) -> bool | None: + def _state(self) -> bool | None: """Return the entity state.""" if (installed_version := self.installed_version) is None or ( latest_version := self.latest_version @@ -338,22 +366,6 @@ async def async_update(self) -> None: # await CoordinatorEntity.async_update(self) await super().async_update() - def get_state(self): - """Get the state for the entity.""" - response = super().get_state() - response["state"] = self.state - response.update(self.state_attributes) - return response - - def to_json(self): - """Return entity in JSON format.""" - response = super().to_json() - response["entity_category"] = self._attr_entity_category - response["device_class"] = self._attr_device_class - response["supported_features"] = self._attr_supported_features - response.update(self.state_attributes) - return response - @functools.lru_cache(maxsize=256) def _version_is_newer(latest_version: str, installed_version: str) -> bool: diff --git a/zha/zigbee/cluster_handlers/__init__.py b/zha/zigbee/cluster_handlers/__init__.py index d8c98d3..b1b224e 100644 --- a/zha/zigbee/cluster_handlers/__init__.py +++ b/zha/zigbee/cluster_handlers/__init__.py @@ -158,6 +158,30 @@ class ClusterConfigureReportingEvent: event: Final[str] = ZHA_CLUSTER_HANDLER_MSG_CFG_RPT +@dataclass(kw_only=True, frozen=True) +class ClusterInfo: + """Cluster information.""" + + id: int + name: str + type: str + commands: dict[int, str] + + +@dataclass(kw_only=True, frozen=True) +class ClusterHandlerInfo: + """Cluster handler information.""" + + class_name: str + generic_id: str + endpoint_id: str + cluster: ClusterInfo + id: str + unique_id: str + status: ClusterHandlerStatus + value_attribute: str | None = None + + class ClusterHandler(LogMixin, EventBase): """Base cluster handler for a Zigbee cluster.""" @@ -192,27 +216,46 @@ def matches(cls, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> bool: # pyl """Filter the cluster match for specific devices.""" return True - @property + @functools.cached_property + def info_object(self) -> ClusterHandlerInfo: + """Return info about this cluster handler.""" + return ClusterHandlerInfo( + class_name=self.__class__.__name__, + generic_id=self._generic_id, + endpoint_id=self._endpoint.id, + cluster=ClusterInfo( + id=self._cluster.cluster_id, + name=self._cluster.name, + type="client" if self._cluster.is_client else "server", + commands=self._cluster.commands, + ), + id=self._id, + unique_id=self._unique_id, + status=self._status.name, + value_attribute=getattr(self, "value_attribute", None), + ) + + @functools.cached_property def id(self) -> str: """Return cluster handler id unique for this device only.""" return self._id - @property + @functools.cached_property def generic_id(self) -> str: """Return the generic id for this cluster handler.""" return self._generic_id - @property + @functools.cached_property def unique_id(self) -> str: """Return the unique id for this cluster handler.""" return self._unique_id - @property + @functools.cached_property def cluster(self) -> zigpy.zcl.Cluster: """Return the zigpy cluster for this cluster handler.""" return self._cluster - @property + @functools.cached_property def name(self) -> str: """Return friendly name.""" return self.cluster.ep_attribute or self._generic_id @@ -585,28 +628,6 @@ async def write_attributes_safe( f"Failed to write attribute {name}={value}: {record.status}", ) - def to_json(self) -> dict: - """Return JSON representation of this cluster handler.""" - json = { - "class_name": self.__class__.__name__, - "generic_id": self._generic_id, - "endpoint_id": self._endpoint.id, - "cluster": { - "id": self._cluster.cluster_id, - "name": self._cluster.name, - "type": "client" if self._cluster.is_client else "server", - "commands": self._cluster.commands, - }, - "id": self._id, - "unique_id": self._unique_id, - "status": self._status.name, - } - - if hasattr(self, "value_attribute"): - json["value_attribute"] = self.value_attribute - - return json - def log(self, level, msg, *args, **kwargs) -> None: """Log a message.""" msg = f"[%s:%s]: {msg}" diff --git a/zha/zigbee/device.py b/zha/zigbee/device.py index 23fdffd..93bac85 100644 --- a/zha/zigbee/device.py +++ b/zha/zigbee/device.py @@ -31,36 +31,19 @@ from zha.application import discovery from zha.application.const import ( - ATTR_ACTIVE_COORDINATOR, ATTR_ARGS, ATTR_ATTRIBUTE, - ATTR_AVAILABLE, ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, ATTR_COMMAND, ATTR_COMMAND_TYPE, - ATTR_DEVICE_TYPE, ATTR_ENDPOINT_ID, - ATTR_ENDPOINT_NAMES, ATTR_ENDPOINTS, - ATTR_IEEE, - ATTR_LAST_SEEN, - ATTR_LQI, ATTR_MANUFACTURER, - ATTR_MANUFACTURER_CODE, ATTR_MODEL, - ATTR_NAME, - ATTR_NEIGHBORS, ATTR_NODE_DESCRIPTOR, - ATTR_NWK, ATTR_PARAMS, - ATTR_POWER_SOURCE, - ATTR_QUIRK_APPLIED, - ATTR_QUIRK_CLASS, ATTR_QUIRK_ID, - ATTR_ROUTES, - ATTR_RSSI, - ATTR_SIGNATURE, ATTR_VALUE, CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, @@ -83,7 +66,7 @@ ZHA_OPTIONS, ) from zha.application.helpers import async_get_zha_config_value, convert_to_zcl_values -from zha.application.platforms import PlatformEntity +from zha.application.platforms import PlatformEntity, PlatformEntityInfo from zha.decorators import periodic from zha.event import EventBase from zha.exceptions import ZHAException @@ -147,6 +130,73 @@ class ClusterHandlerConfigurationComplete: event: Final[str] = ZHA_CLUSTER_HANDLER_CFG_DONE +@dataclass(kw_only=True, frozen=True) +class DeviceInfo: + """Describes a device.""" + + ieee: str + nwk: int + manufacturer: str + model: str + name: str + quirk_applied: bool + quirk_class: str + quirk_id: str | None + manufacturer_code: int | None + power_source: str + lqi: int + rssi: int + last_seen: str + available: bool + device_type: str + signature: dict[str, Any] + + +@dataclass(kw_only=True, frozen=True) +class NeighborInfo: + """Describes a neighbor.""" + + device_type: str + rx_on_when_idle: str + relationship: str + extended_pan_id: str + ieee: str + nwk: str + permit_joining: str + depth: str + lqi: str + + +@dataclass(kw_only=True, frozen=True) +class RouteInfo: + """Describes a route.""" + + dest_nwk: str + route_status: str + memory_constrained: bool + many_to_one: bool + route_record_required: bool + next_hop: str + + +@dataclass(kw_only=True, frozen=True) +class EndpointNameInfo: + """Describes an endpoint name.""" + + name: str + + +@dataclass(kw_only=True, frozen=True) +class ExtendedDeviceInfo(DeviceInfo): + """Describes a ZHA device.""" + + active_coordinator: bool + entities: dict[str, PlatformEntityInfo] + neighbors: list[NeighborInfo] + routes: list[RouteInfo] + endpoint_names: list[EndpointNameInfo] + + class Device(LogMixin, EventBase): """ZHA Zigbee device object.""" @@ -220,36 +270,36 @@ def __init__( getattr(self, "__polling_interval"), ) - @property + @cached_property def device(self) -> zigpy.device.Device: """Return underlying Zigpy device.""" return self._zigpy_device - @property + @cached_property def name(self) -> str: """Return device name.""" return f"{self.manufacturer} {self.model}" - @property + @cached_property def ieee(self) -> EUI64: """Return ieee address for device.""" return self._zigpy_device.ieee - @property + @cached_property def manufacturer(self) -> str: """Return manufacturer for device.""" if self._zigpy_device.manufacturer is None: return UNKNOWN_MANUFACTURER return self._zigpy_device.manufacturer - @property + @cached_property def model(self) -> str: """Return model for device.""" if self._zigpy_device.model is None: return UNKNOWN_MODEL return self._zigpy_device.model - @property + @cached_property def manufacturer_code(self) -> int | None: """Return the manufacturer code for the device.""" if self._zigpy_device.node_desc is None: @@ -257,7 +307,7 @@ def manufacturer_code(self) -> int | None: return self._zigpy_device.node_desc.manufacturer_code - @property + @cached_property def nwk(self) -> NWK: """Return nwk for device.""" return self._zigpy_device.nwk @@ -277,7 +327,7 @@ def last_seen(self) -> float | None: """Return last_seen for device.""" return self._zigpy_device.last_seen - @property + @cached_property def is_mains_powered(self) -> bool | None: """Return true if device is mains powered.""" if self._zigpy_device.node_desc is None: @@ -285,7 +335,7 @@ def is_mains_powered(self) -> bool | None: return self._zigpy_device.node_desc.is_mains_powered - @property + @cached_property def device_type(self) -> str: """Return the logical device type for the device.""" if self._zigpy_device.node_desc is None: @@ -300,7 +350,7 @@ def power_source(self) -> str: POWER_MAINS_POWERED if self.is_mains_powered else POWER_BATTERY_OR_UNKNOWN ) - @property + @cached_property def is_router(self) -> bool | None: """Return true if this is a routing capable device.""" if self._zigpy_device.node_desc is None: @@ -308,7 +358,7 @@ def is_router(self) -> bool | None: return self._zigpy_device.node_desc.is_router - @property + @cached_property def is_coordinator(self) -> bool | None: """Return true if this device represents a coordinator.""" if self._zigpy_device.node_desc is None: @@ -324,7 +374,7 @@ def is_active_coordinator(self) -> bool: return self.ieee == self.gateway.state.node_info.ieee - @property + @cached_property def is_end_device(self) -> bool | None: """Return true if this device is an end device.""" if self._zigpy_device.node_desc is None: @@ -339,12 +389,12 @@ def is_groupable(self) -> bool: self.available and bool(self.async_get_groupable_endpoints()) ) - @property + @cached_property def skip_configuration(self) -> bool: """Return true if the device should not issue configuration related commands.""" return self._zigpy_device.skip_configuration or bool(self.is_coordinator) - @property + @cached_property def gateway(self): """Return the gateway for this device.""" return self._gateway @@ -406,7 +456,7 @@ def identify_ch(self, cluster_handler: ClusterHandler) -> None: if self._identify_ch is None: self._identify_ch = cluster_handler - @property + @cached_property def zdo_cluster_handler(self) -> ZDOClusterHandler: """Return ZDO cluster handler.""" return self._zdo_handler @@ -416,7 +466,7 @@ def endpoints(self) -> dict[int, Endpoint]: """Return the endpoints for this device.""" return self._endpoints - @property + @cached_property def zigbee_signature(self) -> dict[str, Any]: """Get zigbee signature for this device.""" return { @@ -572,29 +622,86 @@ async def _async_became_available(self) -> None: platform_entity.maybe_emit_state_changed_event() @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return a device description for device.""" ieee = str(self.ieee) time_struct = time.localtime(self.last_seen) update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) - return { - ATTR_IEEE: ieee, - ATTR_NWK: self.nwk, - ATTR_MANUFACTURER: self.manufacturer, - ATTR_MODEL: self.model, - ATTR_NAME: self.name or ieee, - ATTR_QUIRK_APPLIED: self.quirk_applied, - ATTR_QUIRK_CLASS: self.quirk_class, - ATTR_QUIRK_ID: self.quirk_id, - ATTR_MANUFACTURER_CODE: self.manufacturer_code, - ATTR_POWER_SOURCE: self.power_source, - ATTR_LQI: self.lqi, - ATTR_RSSI: self.rssi, - ATTR_LAST_SEEN: update_time, - ATTR_AVAILABLE: self.available, - ATTR_DEVICE_TYPE: self.device_type, - ATTR_SIGNATURE: self.zigbee_signature, - } + return DeviceInfo( + ieee=ieee, + nwk=self.nwk, + manufacturer=self.manufacturer, + model=self.model, + name=self.name, + quirk_applied=self.quirk_applied, + quirk_class=self.quirk_class, + quirk_id=self.quirk_id, + manufacturer_code=self.manufacturer_code, + power_source=self.power_source, + lqi=self.lqi, + rssi=self.rssi, + last_seen=update_time, + available=self.available, + device_type=self.device_type, + signature=self.zigbee_signature, + ) + + @property + def extended_device_info(self) -> ExtendedDeviceInfo: + """Get extended device information.""" + topology = self.gateway.application_controller.topology + names: list[EndpointNameInfo] = [] + for endpoint in (ep for epid, ep in self.device.endpoints.items() if epid): + profile = PROFILES.get(endpoint.profile_id) + if profile and endpoint.device_type is not None: + # DeviceType provides undefined enums + names.append( + EndpointNameInfo(name=profile.DeviceType(endpoint.device_type).name) + ) + else: + names.append( + EndpointNameInfo( + name=( + f"unknown {endpoint.device_type} device_type " + f"of 0x{(endpoint.profile_id or 0xFFFF):04x} profile id" + ) + ) + ) + + return ExtendedDeviceInfo( + **self.device_info.__dict__, + active_coordinator=self.is_active_coordinator, + entities={ + unique_id: platform_entity.info_object + for unique_id, platform_entity in self.platform_entities.items() + }, + neighbors=[ + NeighborInfo( + device_type=neighbor.device_type.name, + rx_on_when_idle=neighbor.rx_on_when_idle.name, + relationship=neighbor.relationship.name, + extended_pan_id=str(neighbor.extended_pan_id), + ieee=str(neighbor.ieee), + nwk=str(neighbor.nwk), + permit_joining=neighbor.permit_joining.name, + depth=str(neighbor.depth), + lqi=str(neighbor.lqi), + ) + for neighbor in topology.neighbors[self.ieee] + ], + routes=[ + RouteInfo( + dest_nwk=str(route.DstNWK), + route_status=route.RouteStatus.name, + memory_constrained=bool(route.MemoryConstrained), + many_to_one=bool(route.ManyToOne), + route_record_required=bool(route.RouteRecordRequired), + next_hop=str(route.NextHop), + ) + for route in topology.routes[self.ieee] + ], + endpoint_names=names, + ) async def async_configure(self) -> None: """Configure the device.""" @@ -665,64 +772,6 @@ async def on_remove(self) -> None: for platform_entity in self._platform_entities.values(): await platform_entity.on_remove() - @property - def zha_device_info(self) -> dict[str, Any]: - """Get ZHA device information.""" - device_info: dict[str, Any] = {} - device_info.update(self.device_info) - device_info[ATTR_ACTIVE_COORDINATOR] = self.is_active_coordinator - device_info["entities"] = { - unique_id: platform_entity.to_json() - for unique_id, platform_entity in self.platform_entities.items() - } - - topology = self.gateway.application_controller.topology - device_info[ATTR_NEIGHBORS] = [ - { - "device_type": neighbor.device_type.name, - "rx_on_when_idle": neighbor.rx_on_when_idle.name, - "relationship": neighbor.relationship.name, - "extended_pan_id": str(neighbor.extended_pan_id), - "ieee": str(neighbor.ieee), - "nwk": str(neighbor.nwk), - "permit_joining": neighbor.permit_joining.name, - "depth": str(neighbor.depth), - "lqi": str(neighbor.lqi), - } - for neighbor in topology.neighbors[self.ieee] - ] - - device_info[ATTR_ROUTES] = [ - { - "dest_nwk": str(route.DstNWK), - "route_status": str(route.RouteStatus.name), - "memory_constrained": bool(route.MemoryConstrained), - "many_to_one": bool(route.ManyToOne), - "route_record_required": bool(route.RouteRecordRequired), - "next_hop": str(route.NextHop), - } - for route in topology.routes[self.ieee] - ] - - # Return endpoint device type Names - names: list[dict[str, str]] = [] - for endpoint in (ep for epid, ep in self.device.endpoints.items() if epid): - profile = PROFILES.get(endpoint.profile_id) - if profile and endpoint.device_type is not None: - # DeviceType provides undefined enums - names.append({ATTR_NAME: profile.DeviceType(endpoint.device_type).name}) - else: - names.append( - { - ATTR_NAME: ( - f"unknown {endpoint.device_type} device_type " - f"of 0x{(endpoint.profile_id or 0xFFFF):04x} profile id" - ) - } - ) - device_info[ATTR_ENDPOINT_NAMES] = names - return device_info - def async_get_clusters(self) -> dict[int, dict[str, dict[int, Cluster]]]: """Get all clusters for this device.""" return { diff --git a/zha/zigbee/endpoint.py b/zha/zigbee/endpoint.py index 2ffe4d0..efae727 100644 --- a/zha/zigbee/endpoint.py +++ b/zha/zigbee/endpoint.py @@ -50,7 +50,7 @@ def __init__(self, zigpy_endpoint: ZigpyEndpoint, device: Device) -> None: self._client_cluster_handlers: dict[str, ClientClusterHandler] = {} self._unique_id: str = f"{str(device.ieee)}-{zigpy_endpoint.endpoint_id}" - @property + @functools.cached_property def device(self) -> Device: """Return the device this endpoint belongs to.""" return self._device @@ -70,17 +70,17 @@ def client_cluster_handlers(self) -> dict[str, ClientClusterHandler]: """Return a dict of client cluster handlers.""" return self._client_cluster_handlers - @property + @functools.cached_property def zigpy_endpoint(self) -> ZigpyEndpoint: """Return endpoint of zigpy device.""" return self._zigpy_endpoint - @property + @functools.cached_property def id(self) -> int: """Return endpoint id.""" return self._zigpy_endpoint.endpoint_id - @property + @functools.cached_property def unique_id(self) -> str: """Return the unique id for this endpoint.""" return self._unique_id diff --git a/zha/zigbee/group.py b/zha/zigbee/group.py index 6d87174..a147045 100644 --- a/zha/zigbee/group.py +++ b/zha/zigbee/group.py @@ -5,15 +5,21 @@ import asyncio from collections.abc import Callable from dataclasses import dataclass +from functools import cached_property import logging from typing import TYPE_CHECKING, Any import zigpy.exceptions from zigpy.types.named import EUI64 -from zha.application.platforms import EntityStateChangedEvent, PlatformEntity +from zha.application.platforms import ( + EntityStateChangedEvent, + PlatformEntity, + PlatformEntityInfo, +) from zha.const import STATE_CHANGED from zha.mixins import LogMixin +from zha.zigbee.device import ExtendedDeviceInfo if TYPE_CHECKING: from zigpy.group import Group as ZigpyGroup, GroupEndpoint @@ -42,6 +48,26 @@ class GroupEntityReference: original_name: str | None = None +@dataclass(frozen=True, kw_only=True) +class GroupMemberInfo: + """Describes a group member.""" + + ieee: EUI64 + endpoint_id: int + device_info: ExtendedDeviceInfo + entities: dict[str, PlatformEntityInfo] + + +@dataclass(frozen=True, kw_only=True) +class GroupInfo: + """Describes a group.""" + + group_id: int + name: str + members: list[GroupMemberInfo] + entities: dict[str, PlatformEntityInfo] + + class GroupMember(LogMixin): """Composite object that represents a device endpoint in a Zigbee group.""" @@ -51,36 +77,40 @@ def __init__(self, zha_group: Group, device: Device, endpoint_id: int) -> None: self._device: Device = device self._endpoint_id: int = endpoint_id - @property + @cached_property def group(self) -> Group: """Return the group this member belongs to.""" return self._group - @property + @cached_property def endpoint_id(self) -> int: """Return the endpoint id for this group member.""" return self._endpoint_id - @property + @cached_property def endpoint(self) -> GroupEndpoint: """Return the endpoint for this group member.""" return self._device.device.endpoints.get(self.endpoint_id) - @property + @cached_property def device(self) -> Device: """Return the ZHA device for this group member.""" return self._device - @property - def member_info(self) -> dict[str, Any]: + @cached_property + def member_info(self) -> GroupMemberInfo: """Get ZHA group info.""" - member_info: dict[str, Any] = {} - member_info["endpoint_id"] = self.endpoint_id - member_info["device"] = self.device.zha_device_info - member_info["entities"] = self.associated_entities - return member_info - - @property + return GroupMemberInfo( + ieee=self.device.ieee, + 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 + }, + ) + + @cached_property def associated_entities(self) -> list[PlatformEntity]: """Return the list of entities that were derived from this endpoint.""" return [ @@ -107,16 +137,6 @@ async def async_remove_from_group(self) -> None: str(ex), ) - def to_json(self) -> dict[str, Any]: - """Get group info.""" - member_info: dict[str, Any] = {} - member_info["endpoint_id"] = self.endpoint_id - member_info["device"] = self.device.zha_device_info - member_info["entities"] = { - entity.unique_id: entity.to_json() for entity in self.associated_entities - } - return member_info - def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" @@ -143,32 +163,32 @@ def name(self) -> str: """Return group name.""" return self._zigpy_group.name - @property + @cached_property def group_id(self) -> int: """Return group name.""" return self._zigpy_group.group_id - @property + @cached_property def endpoint(self) -> zigpy.endpoint.Endpoint: """Return the endpoint for this group.""" return self._zigpy_group.endpoint - @property + @cached_property def group_entities(self) -> dict[str, GroupEntity]: """Return the platform entities of the group.""" return self._group_entities - @property + @cached_property def zigpy_group(self) -> ZigpyGroup: """Return the zigpy group.""" return self._zigpy_group - @property + @cached_property def gateway(self) -> Gateway: """Return the gateway for this group.""" return self._gateway - @property + @cached_property def members(self) -> list[GroupMember]: """Return the ZHA devices that are members of this group.""" return [ @@ -177,6 +197,29 @@ def members(self) -> list[GroupMember]: if member_ieee in self._gateway.devices ] + @cached_property + def info_object(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 + for unique_id, entity in self._group_entities.items() + }, + ) + + @cached_property + def all_member_entity_unique_ids(self) -> list[str]: + """Return all platform entities unique ids for the members of this group.""" + all_entity_unique_ids: list[str] = [] + for member in self.members: + entities = member.associated_entities + for entity in entities: + all_entity_unique_ids.append(entity.unique_id) + return all_entity_unique_ids + def register_group_entity(self, group_entity: GroupEntity) -> None: """Register a group entity.""" if group_entity.unique_id not in self._group_entities: @@ -187,15 +230,6 @@ def register_group_entity(self, group_entity: GroupEntity) -> None: ) self.update_entity_subscriptions() - @property - def group_info(self) -> dict[str, Any]: - """Get ZHA group info.""" - group_info: dict[str, Any] = {} - group_info["group_id"] = self.group_id - group_info["name"] = self.name - group_info["members"] = [member.member_info for member in self.members] - return group_info - async def _maybe_update_group_members(self, event: EntityStateChangedEvent) -> None: """Update the state of the entities that make up the group if they are marked as should poll.""" tasks = [] @@ -206,6 +240,17 @@ async def _maybe_update_group_members(self, event: EntityStateChangedEvent) -> N if tasks: await asyncio.gather(*tasks) + 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, "members"): + delattr(self, "members") + if hasattr(self, "group_entities"): + delattr(self, "group_entities") + def update_entity_subscriptions(self) -> None: """Update the entity event subscriptions. @@ -216,6 +261,8 @@ def update_entity_subscriptions(self) -> None: for group entities and the platrom entities that we processed. Then we loop over all of the unsub ids and we execute the unsubscribe method for each one that isn't in the combined list. """ + self.clear_caches() + group_entity_ids = list(self._group_entities.keys()) processed_platform_entity_ids = [] for group_entity in self._group_entities.values(): @@ -277,16 +324,6 @@ async def async_remove_members(self, members: list[GroupMemberReference]) -> Non ) self.update_entity_subscriptions() - @property - def all_member_entity_unique_ids(self) -> list[str]: - """Return all platform entities unique ids for the members of this group.""" - all_entity_unique_ids: list[str] = [] - for member in self.members: - entities = member.associated_entities - for entity in entities: - all_entity_unique_ids.append(entity.unique_id) - return all_entity_unique_ids - def get_platform_entities(self, platform: str) -> list[PlatformEntity]: """Return entities belonging to the specified platform for this group.""" platform_entities: list[PlatformEntity] = [] @@ -299,20 +336,6 @@ def get_platform_entities(self, platform: str) -> list[PlatformEntity]: return platform_entities - def to_json(self) -> dict[str, Any]: - """Get ZHA group info.""" - group_info: dict[str, Any] = {} - group_info["id"] = self.group_id - group_info["name"] = self.name - group_info["members"] = { - str(member.device.ieee): member.to_json() for member in self.members - } - group_info["entities"] = { - unique_id: entity.to_json() - for unique_id, entity in self._group_entities.items() - } - return group_info - def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}"