Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute primary entities #298

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
SIG_EP_OUTPUT,
SIG_EP_TYPE,
create_mock_zigpy_device,
get_entity,
join_zigpy_device,
zigpy_device_from_json,
)
Expand All @@ -34,6 +35,9 @@
UNKNOWN,
)
from zha.application.gateway import Gateway
from zha.application.platforms import PlatformEntity
from zha.application.platforms.binary_sensor import IASZone
from zha.application.platforms.light import Light
from zha.application.platforms.sensor import LQISensor, RSSISensor
from zha.application.platforms.switch import Switch
from zha.exceptions import ZHAException
Expand Down Expand Up @@ -820,3 +824,63 @@ async def test_quirks_v2_device_renaming(zha_gateway: Gateway) -> None:
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
assert zha_device.model == "IRIS Keypad V2"
assert zha_device.manufacturer == "Lowe's"


@pytest.mark.parametrize(
("json_path", "primary_platform", "primary_entity_type"),
[
# Light bulb
(
"tests/data/devices/ikea-of-sweden-tradfri-bulb-gu10-ws-400lm.json",
Platform.LIGHT,
Light,
),
# Night light with a bulb and a motion sensor
(
"tests/data/devices/third-reality-inc-3rsnl02043z.json",
Platform.LIGHT,
Light,
),
# Door sensor
(
"tests/data/devices/centralite-3320-l.json",
Platform.BINARY_SENSOR,
IASZone,
),
# Smart plug with energy monitoring
(
"tests/data/devices/innr-sp-234.json",
Platform.SWITCH,
Switch,
),
# Atmosphere sensor with humidity, temperature, and pressure
(
"tests/data/devices/lumi-lumi-weather.json",
None,
None,
),
],
)
async def test_primary_entity_computation(
json_path: str,
primary_platform: Platform | None,
primary_entity_type: PlatformEntity | None,
zha_gateway: Gateway,
) -> None:
"""Test primary entity computation."""

zigpy_dev = await zigpy_device_from_json(
zha_gateway.application_controller,
json_path,
)
zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)

# There is a single light entity
primary = [e for e in zha_device.platform_entities.values() if e.primary]

if primary_platform is None:
assert not primary
else:
assert primary == [
get_entity(zha_device, primary_platform, entity_type=primary_entity_type)
]
7 changes: 7 additions & 0 deletions zha/application/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ def load_groups(self) -> None:
def create_platform_entities(self) -> None:
"""Create platform entities."""

entity_devices = set()

for platform in discovery.PLATFORMS:
for platform_entity_class, args, kw_args in self.config.platforms[platform]:
try:
Expand All @@ -347,8 +349,13 @@ def create_platform_entities(self) -> None:
_LOGGER.debug(
"Platform entity data: %s", platform_entity.info_object
)
entity_devices.add(platform_entity.device)
self.config.platforms[platform].clear()

# Compute primary entities after they've all been created
for device in entity_devices:
device._compute_primary_entity()

@property
def radio_concurrency(self) -> int:
"""Maximum configured radio concurrency."""
Expand Down
22 changes: 22 additions & 0 deletions zha/application/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class BaseEntityInfo:
entity_category: str | None
entity_registry_enabled_default: bool
enabled: bool = True
primary: bool

# For platform entities
cluster_handlers: list[ClusterHandlerInfo]
Expand Down Expand Up @@ -117,6 +118,11 @@ class BaseEntity(LogMixin, EventBase):
_attr_device_class: str | None
_attr_state_class: str | None
_attr_enabled: bool = True
_attr_primary: bool = False

# When two entities both want to be primary, the one with the higher weight will be
# chosen. If there is a tie, both lose.
_attr_primary_weight: int = 0

def __init__(self, unique_id: str) -> None:
"""Initialize the platform entity."""
Expand All @@ -138,6 +144,21 @@ def enabled(self, value: bool) -> None:
"""Set the entity enabled state."""
self._attr_enabled = value

@property
def primary(self) -> bool:
"""Return if the entity is the primary device control."""
return self._attr_primary

@primary.setter
def primary(self, value: bool) -> None:
"""Set the entity as the primary device control."""
self._attr_primary = value

@property
def primary_weight(self) -> int:
"""Return the primary weight of the entity."""
return self._attr_primary_weight

@property
def fallback_name(self) -> str | None:
"""Return the entity fallback name for when a translation key is unavailable."""
Expand Down Expand Up @@ -212,6 +233,7 @@ def info_object(self) -> BaseEntityInfo:
entity_category=self.entity_category,
entity_registry_enabled_default=self.entity_registry_enabled_default,
enabled=self.enabled,
primary=self.primary,
# Set by platform entities
cluster_handlers=[],
device_ieee=None,
Expand Down
5 changes: 5 additions & 0 deletions zha/application/platforms/binary_sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@ class Occupancy(BinarySensor):

_attribute_name = "occupancy"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY
_attr_primary_weight = 2


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

_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY
_attr_primary_weight = 3


@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF)
Expand All @@ -170,6 +172,7 @@ class Opening(BinarySensor):

_attribute_name = "on_off"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BINARY_INPUT)
Expand Down Expand Up @@ -203,6 +206,7 @@ class IASZone(BinarySensor):
"""ZHA IAS BinarySensor."""

_attribute_name = "zone_status"
_attr_primary_weight = 3

def __init__(
self,
Expand Down Expand Up @@ -247,6 +251,7 @@ class SinopeLeakStatus(BinarySensor):

_attribute_name = "leak_status"
_attr_device_class = BinarySensorDeviceClass.MOISTURE
_attr_primary_weight = 1


@MULTI_MATCH(
Expand Down
1 change: 1 addition & 0 deletions zha/application/platforms/climate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class Thermostat(PlatformEntity):
ATTR_UNOCCP_COOL_SETPT,
ATTR_UNOCCP_HEAT_SETPT,
}
_attr_primary_weight = 10

def __init__(
self,
Expand Down
2 changes: 2 additions & 0 deletions zha/application/platforms/cover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Cover(PlatformEntity):
"target_lift_position",
"target_tilt_position",
}
_attr_primary_weight = 10

def __init__(
self,
Expand Down Expand Up @@ -373,6 +374,7 @@ class Shade(PlatformEntity):

_attr_device_class = CoverDeviceClass.SHADE
_attr_translation_key: str = "shade"
_attr_primary_weight = 10

def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions zha/application/platforms/fan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class BaseFan(BaseEntity):
| FanEntityFeature.TURN_ON
)
_attr_translation_key: str = "fan"
_attr_primary_weight = 10

@functools.cached_property
def preset_modes(self) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions zha/application/platforms/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class BaseLight(BaseEntity, ABC):
"off_with_transition",
"off_brightness",
}
_attr_primary_weight = 10

def __init__(self, *args, **kwargs):
"""Initialize the light."""
Expand Down
1 change: 1 addition & 0 deletions zha/application/platforms/lock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class DoorLock(PlatformEntity):

PLATFORM = Platform.LOCK
_attr_translation_key: str = "door_lock"
_attr_primary_weight = 10

def __init__(
self,
Expand Down
15 changes: 15 additions & 0 deletions zha/application/platforms/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,7 @@ class Humidity(Sensor):
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_divisor = 100
_attr_native_unit_of_measurement = PERCENTAGE
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_SOIL_MOISTURE)
Expand All @@ -751,6 +752,7 @@ class SoilMoisture(Sensor):
_attr_translation_key: str = "soil_moisture"
_divisor = 100
_attr_native_unit_of_measurement = PERCENTAGE
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEAF_WETNESS)
Expand All @@ -763,6 +765,7 @@ class LeafWetness(Sensor):
_attr_translation_key: str = "leaf_wetness"
_divisor = 100
_attr_native_unit_of_measurement = PERCENTAGE
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ILLUMINANCE)
Expand All @@ -773,6 +776,7 @@ class Illuminance(Sensor):
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ILLUMINANCE
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = LIGHT_LUX
_attr_primary_weight = 1

def formatter(self, value: int) -> int | None:
"""Convert illumination data."""
Expand Down Expand Up @@ -810,6 +814,7 @@ class SmartEnergyMetering(PollableSensor):
"status",
"zcl_unit_of_measurement",
}
_attr_primary_weight = 1

_ENTITY_DESCRIPTION_MAP = {
0x00: SmartEnergyMeteringEntityDescription(
Expand Down Expand Up @@ -1131,6 +1136,7 @@ class Pressure(Sensor):
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_attr_native_unit_of_measurement = UnitOfPressure.HPA
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_FLOW)
Expand All @@ -1142,6 +1148,7 @@ class Flow(Sensor):
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_divisor = 10
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
_attr_primary_weight = 1

def formatter(self, value: int) -> datetime | int | float | str | None:
"""Handle unknown value state."""
Expand All @@ -1159,6 +1166,7 @@ class Temperature(Sensor):
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_divisor = 100
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DEVICE_TEMPERATURE)
Expand All @@ -1172,6 +1180,7 @@ class DeviceTemperature(Sensor):
_divisor = 100
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
Expand Down Expand Up @@ -1214,6 +1223,7 @@ class CarbonDioxideConcentration(Sensor):
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names="carbon_monoxide_concentration")
Expand All @@ -1226,6 +1236,7 @@ class CarbonMonoxideConcentration(Sensor):
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
_attr_primary_weight = 1


@MULTI_MATCH(generic_ids="cluster_handler_0x042e", stop_on_match_group="voc_level")
Expand All @@ -1239,6 +1250,7 @@ class VOCLevel(Sensor):
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
_attr_primary_weight = 1


@MULTI_MATCH(
Expand All @@ -1257,6 +1269,7 @@ class PPBVOCLevel(Sensor):
_decimals = 0
_multiplier = 1
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names="pm25")
Expand All @@ -1269,6 +1282,7 @@ class PM25(Sensor):
_decimals = 0
_multiplier = 1
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
_attr_primary_weight = 1


@MULTI_MATCH(cluster_handler_names="formaldehyde_concentration")
Expand All @@ -1281,6 +1295,7 @@ class FormaldehydeConcentration(Sensor):
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
_attr_primary_weight = 1


@MULTI_MATCH(
Expand Down
1 change: 1 addition & 0 deletions zha/application/platforms/siren.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class Siren(PlatformEntity):

PLATFORM = Platform.SIREN
_attr_fallback_name: str = "Siren"
_attr_primary_weight = 10

def __init__(
self,
Expand Down
2 changes: 2 additions & 0 deletions zha/application/platforms/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class BaseSwitch(BaseEntity, ABC):
"""Common base class for zhawss switches."""

PLATFORM = Platform.SWITCH
_attr_primary_weight = 10

def __init__(
self,
Expand Down Expand Up @@ -106,6 +107,7 @@ class Switch(PlatformEntity, BaseSwitch):
"""ZHA switch."""

_attr_translation_key = "switch"
_attr_primary_weight = 10

def __init__(
self,
Expand Down
Loading