Skip to content

Commit 29db30d

Browse files
authored
Compute primary entities (#298)
1 parent 7ac35c9 commit 29db30d

File tree

12 files changed

+154
-0
lines changed

12 files changed

+154
-0
lines changed

tests/test_device.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
SIG_EP_OUTPUT,
2222
SIG_EP_TYPE,
2323
create_mock_zigpy_device,
24+
get_entity,
2425
join_zigpy_device,
2526
zigpy_device_from_json,
2627
)
@@ -34,6 +35,9 @@
3435
UNKNOWN,
3536
)
3637
from zha.application.gateway import Gateway
38+
from zha.application.platforms import PlatformEntity
39+
from zha.application.platforms.binary_sensor import IASZone
40+
from zha.application.platforms.light import Light
3741
from zha.application.platforms.sensor import LQISensor, RSSISensor
3842
from zha.application.platforms.switch import Switch
3943
from zha.exceptions import ZHAException
@@ -853,3 +857,63 @@ async def test_quirks_v2_device_alerts(zha_gateway: Gateway) -> None:
853857
assert zha_device.device_alerts == (
854858
DeviceAlertMetadata(level=DeviceAlertLevel.WARNING, message="Test warning"),
855859
)
860+
861+
862+
@pytest.mark.parametrize(
863+
("json_path", "primary_platform", "primary_entity_type"),
864+
[
865+
# Light bulb
866+
(
867+
"tests/data/devices/ikea-of-sweden-tradfri-bulb-gu10-ws-400lm.json",
868+
Platform.LIGHT,
869+
Light,
870+
),
871+
# Night light with a bulb and a motion sensor
872+
(
873+
"tests/data/devices/third-reality-inc-3rsnl02043z.json",
874+
Platform.LIGHT,
875+
Light,
876+
),
877+
# Door sensor
878+
(
879+
"tests/data/devices/centralite-3320-l.json",
880+
Platform.BINARY_SENSOR,
881+
IASZone,
882+
),
883+
# Smart plug with energy monitoring
884+
(
885+
"tests/data/devices/innr-sp-234.json",
886+
Platform.SWITCH,
887+
Switch,
888+
),
889+
# Atmosphere sensor with humidity, temperature, and pressure
890+
(
891+
"tests/data/devices/lumi-lumi-weather.json",
892+
None,
893+
None,
894+
),
895+
],
896+
)
897+
async def test_primary_entity_computation(
898+
json_path: str,
899+
primary_platform: Platform | None,
900+
primary_entity_type: PlatformEntity | None,
901+
zha_gateway: Gateway,
902+
) -> None:
903+
"""Test primary entity computation."""
904+
905+
zigpy_dev = await zigpy_device_from_json(
906+
zha_gateway.application_controller,
907+
json_path,
908+
)
909+
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
910+
911+
# There is a single light entity
912+
primary = [e for e in zha_device.platform_entities.values() if e.primary]
913+
914+
if primary_platform is None:
915+
assert not primary
916+
else:
917+
assert primary == [
918+
get_entity(zha_device, primary_platform, entity_type=primary_entity_type)
919+
]

zha/application/platforms/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class BaseEntityInfo:
5959
entity_category: str | None
6060
entity_registry_enabled_default: bool
6161
enabled: bool = True
62+
primary: bool
6263

6364
# For platform entities
6465
cluster_handlers: list[ClusterHandlerInfo]
@@ -119,6 +120,11 @@ class BaseEntity(LogMixin, EventBase):
119120
_attr_state_class: str | None
120121
_attr_enabled: bool = True
121122
_attr_always_supported: bool = False
123+
_attr_primary: bool = False
124+
125+
# When two entities both want to be primary, the one with the higher weight will be
126+
# chosen. If there is a tie, both lose.
127+
_attr_primary_weight: int = 0
122128

123129
def __init__(self, unique_id: str) -> None:
124130
"""Initialize the platform entity."""
@@ -156,6 +162,21 @@ def enabled(self, value: bool) -> None:
156162
"""Set the entity enabled state."""
157163
self._attr_enabled = value
158164

165+
@property
166+
def primary(self) -> bool:
167+
"""Return if the entity is the primary device control."""
168+
return self._attr_primary
169+
170+
@primary.setter
171+
def primary(self, value: bool) -> None:
172+
"""Set the entity as the primary device control."""
173+
self._attr_primary = value
174+
175+
@property
176+
def primary_weight(self) -> int:
177+
"""Return the primary weight of the entity."""
178+
return self._attr_primary_weight
179+
159180
@property
160181
def fallback_name(self) -> str | None:
161182
"""Return the entity fallback name for when a translation key is unavailable."""
@@ -230,6 +251,7 @@ def info_object(self) -> BaseEntityInfo:
230251
entity_category=self.entity_category,
231252
entity_registry_enabled_default=self.entity_registry_enabled_default,
232253
enabled=self.enabled,
254+
primary=self.primary,
233255
# Set by platform entities
234256
cluster_handlers=[],
235257
device_ieee=None,

zha/application/platforms/binary_sensor/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,15 @@ class Occupancy(BinarySensor):
167167

168168
_attribute_name = "occupancy"
169169
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY
170+
_attr_primary_weight = 2
170171

171172

172173
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_HUE_OCCUPANCY)
173174
class HueOccupancy(Occupancy):
174175
"""ZHA Hue occupancy."""
175176

176177
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY
178+
_attr_primary_weight = 3
177179

178180

179181
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF)
@@ -182,6 +184,7 @@ class Opening(BinarySensor):
182184

183185
_attribute_name = "on_off"
184186
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING
187+
_attr_primary_weight = 1
185188

186189

187190
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BINARY_INPUT)
@@ -215,6 +218,7 @@ class IASZone(BinarySensor):
215218
"""ZHA IAS BinarySensor."""
216219

217220
_attribute_name = "zone_status"
221+
_attr_primary_weight = 3
218222

219223
def __init__(
220224
self,
@@ -259,6 +263,7 @@ class SinopeLeakStatus(BinarySensor):
259263

260264
_attribute_name = "leak_status"
261265
_attr_device_class = BinarySensorDeviceClass.MOISTURE
266+
_attr_primary_weight = 1
262267

263268

264269
@MULTI_MATCH(

zha/application/platforms/climate/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class Thermostat(PlatformEntity):
9494
ATTR_UNOCCP_COOL_SETPT,
9595
ATTR_UNOCCP_HEAT_SETPT,
9696
}
97+
_attr_primary_weight = 10
9798

9899
def __init__(
99100
self,

zha/application/platforms/cover/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class Cover(PlatformEntity):
6161
"target_lift_position",
6262
"target_tilt_position",
6363
}
64+
_attr_primary_weight = 10
6465

6566
def __init__(
6667
self,
@@ -379,6 +380,7 @@ class Shade(PlatformEntity):
379380

380381
_attr_device_class = CoverDeviceClass.SHADE
381382
_attr_translation_key: str = "shade"
383+
_attr_primary_weight = 10
382384

383385
def __init__(
384386
self,

zha/application/platforms/fan/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class BaseFan(BaseEntity):
8080
| FanEntityFeature.TURN_ON
8181
)
8282
_attr_translation_key: str = "fan"
83+
_attr_primary_weight = 10
8384

8485
@functools.cached_property
8586
def preset_modes(self) -> list[str]:

zha/application/platforms/light/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class BaseLight(BaseEntity, ABC):
108108
"off_with_transition",
109109
"off_brightness",
110110
}
111+
_attr_primary_weight = 10
111112

112113
def __init__(self, *args, **kwargs):
113114
"""Initialize the light."""

zha/application/platforms/lock/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class DoorLock(PlatformEntity):
3636

3737
PLATFORM = Platform.LOCK
3838
_attr_translation_key: str = "door_lock"
39+
_attr_primary_weight = 10
3940

4041
def __init__(
4142
self,

zha/application/platforms/sensor/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ class Humidity(Sensor):
840840
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
841841
_divisor = 100
842842
_attr_native_unit_of_measurement = PERCENTAGE
843+
_attr_primary_weight = 1
843844

844845

845846
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_SOIL_MOISTURE)
@@ -852,6 +853,7 @@ class SoilMoisture(Sensor):
852853
_attr_translation_key: str = "soil_moisture"
853854
_divisor = 100
854855
_attr_native_unit_of_measurement = PERCENTAGE
856+
_attr_primary_weight = 1
855857

856858

857859
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEAF_WETNESS)
@@ -864,6 +866,7 @@ class LeafWetness(Sensor):
864866
_attr_translation_key: str = "leaf_wetness"
865867
_divisor = 100
866868
_attr_native_unit_of_measurement = PERCENTAGE
869+
_attr_primary_weight = 1
867870

868871

869872
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ILLUMINANCE)
@@ -874,6 +877,7 @@ class Illuminance(Sensor):
874877
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ILLUMINANCE
875878
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
876879
_attr_native_unit_of_measurement = LIGHT_LUX
880+
_attr_primary_weight = 1
877881

878882
def formatter(self, value: int) -> int | None:
879883
"""Convert illumination data."""
@@ -911,6 +915,7 @@ class SmartEnergyMetering(PollableSensor):
911915
"status",
912916
"zcl_unit_of_measurement",
913917
}
918+
_attr_primary_weight = 1
914919

915920
_ENTITY_DESCRIPTION_MAP = {
916921
0x00: SmartEnergyMeteringEntityDescription(
@@ -1221,6 +1226,7 @@ class Pressure(Sensor):
12211226
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
12221227
_decimals = 0
12231228
_attr_native_unit_of_measurement = UnitOfPressure.HPA
1229+
_attr_primary_weight = 1
12241230

12251231

12261232
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_FLOW)
@@ -1232,6 +1238,7 @@ class Flow(Sensor):
12321238
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
12331239
_divisor = 10
12341240
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
1241+
_attr_primary_weight = 1
12351242

12361243
def formatter(self, value: int) -> datetime | int | float | str | None:
12371244
"""Handle unknown value state."""
@@ -1249,6 +1256,7 @@ class Temperature(Sensor):
12491256
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
12501257
_divisor = 100
12511258
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
1259+
_attr_primary_weight = 1
12521260

12531261

12541262
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DEVICE_TEMPERATURE)
@@ -1262,6 +1270,7 @@ class DeviceTemperature(Sensor):
12621270
_divisor = 100
12631271
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
12641272
_attr_entity_category = EntityCategory.DIAGNOSTIC
1273+
_attr_primary_weight = 1
12651274

12661275

12671276
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
@@ -1304,6 +1313,7 @@ class CarbonDioxideConcentration(Sensor):
13041313
_decimals = 0
13051314
_multiplier = 1e6
13061315
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
1316+
_attr_primary_weight = 1
13071317

13081318

13091319
@MULTI_MATCH(cluster_handler_names="carbon_monoxide_concentration")
@@ -1316,6 +1326,7 @@ class CarbonMonoxideConcentration(Sensor):
13161326
_decimals = 0
13171327
_multiplier = 1e6
13181328
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
1329+
_attr_primary_weight = 1
13191330

13201331

13211332
@MULTI_MATCH(generic_ids="cluster_handler_0x042e", stop_on_match_group="voc_level")
@@ -1329,6 +1340,7 @@ class VOCLevel(Sensor):
13291340
_decimals = 0
13301341
_multiplier = 1e6
13311342
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
1343+
_attr_primary_weight = 1
13321344

13331345

13341346
@MULTI_MATCH(
@@ -1347,6 +1359,7 @@ class PPBVOCLevel(Sensor):
13471359
_decimals = 0
13481360
_multiplier = 1
13491361
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION
1362+
_attr_primary_weight = 1
13501363

13511364

13521365
@MULTI_MATCH(cluster_handler_names="pm25")
@@ -1359,6 +1372,7 @@ class PM25(Sensor):
13591372
_decimals = 0
13601373
_multiplier = 1
13611374
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
1375+
_attr_primary_weight = 1
13621376

13631377

13641378
@MULTI_MATCH(cluster_handler_names="formaldehyde_concentration")
@@ -1371,6 +1385,7 @@ class FormaldehydeConcentration(Sensor):
13711385
_decimals = 0
13721386
_multiplier = 1e6
13731387
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
1388+
_attr_primary_weight = 1
13741389

13751390

13761391
@MULTI_MATCH(

zha/application/platforms/siren.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class Siren(PlatformEntity):
6868

6969
PLATFORM = Platform.SIREN
7070
_attr_fallback_name: str = "Siren"
71+
_attr_primary_weight = 10
7172

7273
def __init__(
7374
self,

0 commit comments

Comments
 (0)