Skip to content

Commit

Permalink
ZHA events and device triggers (#33)
Browse files Browse the repository at this point in the history
* start implementing zha event

* remove stale todo

* wrong word

* add unique_id

* add device automation triggers

* proxy zha events to devices
  • Loading branch information
dmulcahey authored Feb 13, 2022
1 parent 0550895 commit 40d586f
Show file tree
Hide file tree
Showing 12 changed files with 68 additions and 46 deletions.
11 changes: 11 additions & 0 deletions zhaws/client/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
GroupRemovedEvent,
PlatformEntityStateChangedEvent,
RawDeviceInitializedEvent,
ZHAEvent,
)
from zhaws.client.proxy import DeviceProxy, GroupProxy
from zhaws.event import EventBase
Expand Down Expand Up @@ -88,6 +89,7 @@ def __init__(
self._client.on_event(
EventTypes.PLATFORM_ENTITY_EVENT, self._handle_event_protocol
)
self._client.on_event(EventTypes.DEVICE_EVENT, self._handle_event_protocol)
self._client.on_event(EventTypes.CONTROLLER_EVENT, self._handle_event_protocol)

@property
Expand Down Expand Up @@ -160,6 +162,15 @@ def handle_platform_entity_state_changed(
return
group.emit_platform_entity_event(event)

def handle_zha_event(self, event: ZHAEvent) -> None:
"""Handle a zha_event from the websocket server."""
_LOGGER.debug("zha_event: %s", event)
device = self.devices.get(event.device.ieee)
if device is None:
_LOGGER.warning("Received zha_event from unknown device: %s", event)
return
device.emit("zha_event", event)

def handle_device_joined(self, event: DeviceJoinedEvent) -> None:
"""Handle device joined.
Expand Down
15 changes: 14 additions & 1 deletion zhaws/client/model/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class MinimalEndpoint(BaseModel):
"""Minimal endpoint model."""

id: int
unique_id: str


class MinimalDevice(BaseModel):
Expand Down Expand Up @@ -82,7 +83,6 @@ class MinimalGroup(BaseModel):
class PlatformEntityStateChangedEvent(BaseEvent):
"""Platform entity event."""

"""TODO use this as a base and create specific events for each entity type where state and attributes is fully modeled out"""
event_type: Literal["platform_entity_event"] = "platform_entity_event"
event: Literal["platform_entity_state_changed"] = "platform_entity_state_changed"
platform_entity: MinimalPlatformEntity
Expand Down Expand Up @@ -196,6 +196,18 @@ class DeviceOnlineEvent(BaseEvent):
device: MinimalDevice


class ZHAEvent(BaseEvent):
"""ZHA event."""

event: Literal["zha_event"] = "zha_event"
event_type: Literal["device_event"] = "device_event"
device: MinimalDevice
cluster_handler: MinimalClusterHandler
endpoint: MinimalEndpoint
command: str
args: Union[list, dict]


class GroupRemovedEvent(ControllerEvent):
"""Group removed event."""

Expand Down Expand Up @@ -240,6 +252,7 @@ class GroupMemberRemovedEvent(ControllerEvent):
GroupMemberRemovedEvent,
DeviceOfflineEvent,
DeviceOnlineEvent,
ZHAEvent,
],
Field(discriminator="event"), # noqa: F821
]
2 changes: 2 additions & 0 deletions zhaws/client/model/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Endpoint(BaseModel):
"""Endpoint model."""

id: int
unique_id: str


class GenericState(BaseModel):
Expand Down Expand Up @@ -464,6 +465,7 @@ class Device(BaseDevice):
],
]
neighbors: list[Any]
device_automation_triggers: dict[str, dict[str, Any]]


class GroupEntity(BaseEntity):
Expand Down
11 changes: 10 additions & 1 deletion zhaws/client/proxy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Proxy object for the client side objects."""
from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from zhaws.client.model.events import PlatformEntityStateChangedEvent
from zhaws.client.model.types import ButtonEntity
Expand Down Expand Up @@ -87,5 +87,14 @@ def device_model(self, device_model: DeviceModel) -> None:
"""Set the device model."""
self._proxied_object = device_model

@property
def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, Any]]:
"""Return the device automation triggers."""
model_triggers = self._proxied_object.device_automation_triggers
return {
(key.split("~")[0], key.split("~")[1]): value
for key, value in model_triggers.items()
}

def __repr__(self) -> str:
return self._proxied_object.__repr__()
8 changes: 8 additions & 0 deletions zhaws/server/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ class RawZCLEvents(StrEnum):
ATTRIBUTE_UPDATED = "attribute_updated"


class DeviceEvents(StrEnum):
"""Events that devices can broadcast."""

DEVICE_OFFLINE = "device_offline"
DEVICE_ONLINE = "device_online"
ZHA_EVENT = "zha_event"


ATTR_UNIQUE_ID: Final[str] = "unique_id"
COMMAND: Final[str] = "command"
CONF_BAUDRATE: Final[str] = "baudrate"
Expand Down
1 change: 1 addition & 0 deletions zhaws/server/platforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ def send_event(self, signal: dict[str, Any]) -> None:
}
signal["endpoint"] = {
"id": self._endpoint.id,
"unique_id": self._endpoint.unique_id,
}
_LOGGER.info("Sending event from platform entity: %s", signal)
self.device.send_event(signal)
Expand Down
2 changes: 1 addition & 1 deletion zhaws/server/platforms/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def __init__(self) -> None:
def discover_entities(self, endpoint: Endpoint) -> None:
"""Process an endpoint on a zigpy device."""
_LOGGER.info(
"Discovering entitied for endpoint: %s-%s",
"Discovering entities for endpoint: %s-%s",
str(endpoint.device.ieee),
endpoint.id,
)
Expand Down
27 changes: 11 additions & 16 deletions zhaws/server/zigbee/cluster/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from zhaws.event import EventBase
from zhaws.model import BaseEvent
from zhaws.server.const import EVENT, EVENT_TYPE, EventTypes
from zhaws.server.const import EVENT, EVENT_TYPE, DeviceEvents, EventTypes
from zhaws.server.util import LogMixin
from zhaws.server.zigbee.cluster.const import (
CLUSTER_HANDLER_ZDO,
Expand Down Expand Up @@ -350,18 +350,16 @@ def attribute_updated(self, attrid: int, value: Any) -> None:
def zdo_command(self, *args: Any, **kwargs: Any) -> None:
"""Handle ZDO commands on this cluster."""

def zha_send_event(self, command: str, args: int | dict) -> None:
def zha_send_event(self, command: str, args: list | dict) -> None:
"""Relay events to hass."""
""" TODO
self._ch_pool.zha_send_event(
self.send_event(
{
ATTR_UNIQUE_ID: self.unique_id,
ATTR_CLUSTER_ID: self.cluster.cluster_id,
ATTR_COMMAND: command,
ATTR_ARGS: args,
EVENT: DeviceEvents.ZHA_EVENT,
EVENT_TYPE: EventTypes.DEVICE_EVENT,
"command": command,
"args": args,
}
)
"""

async def async_update(self) -> None:
"""Retrieve latest state from cluster."""
Expand Down Expand Up @@ -512,18 +510,15 @@ class ClientClusterHandler(ClusterHandler):

def attribute_updated(self, attrid: int, value: Any) -> None:
"""Handle an attribute updated on this cluster."""
""" TODO

self.zha_send_event(
SIGNAL_ATTR_UPDATED,
{
ATTR_ATTRIBUTE_ID: attrid,
ATTR_ATTRIBUTE_NAME: self._cluster.attributes.get(attrid, ["Unknown"])[
0
],
ATTR_VALUE: value,
"attribute_id": attrid,
"attribute_name": self._cluster.attributes.get(attrid, ["Unknown"])[0],
"value": value,
},
)
"""

def cluster_command(self, tsn: int, command_id: int, args: Any) -> None:
"""Handle a cluster command received on this cluster."""
Expand Down
2 changes: 1 addition & 1 deletion zhaws/server/zigbee/cluster/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ def cluster_command(
"""Handle commands received to this cluster."""
cmd_name = self.cluster.client_commands.get(command_id, [command_id])[0]
self.debug("Received %s tsn command '%s': %s", tsn, cmd_name, args)
# TODO self.zha_send_event(cmd_name, args)
self.zha_send_event(cmd_name, args or [])
if cmd_name == "checkin":
self.cluster.create_catching_task(self.check_in_response(tsn))

Expand Down
5 changes: 1 addition & 4 deletions zhaws/server/zigbee/cluster/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ def arm(self, arm_mode: int, code: str | None, zone_id: int) -> None:
"""Handle the IAS ACE arm command."""
mode = AceCluster.ArmMode(arm_mode)

"""TODO figure out events
self.zha_send_event(
self._cluster.server_commands.get(IAS_ACE_ARM)[NAME],
{
Expand All @@ -133,7 +132,6 @@ def arm(self, arm_mode: int, code: str | None, zone_id: int) -> None:
"zone_id": zone_id,
},
)
"""

zigbee_reply = self.arm_map[mode](code)
asyncio.create_task(zigbee_reply)
Expand Down Expand Up @@ -218,12 +216,11 @@ def _handle_arm(

def _bypass(self, zone_list: Any, code: str) -> asyncio.Future:
"""Handle the IAS ACE bypass command."""
"""TODO figure out events

self.zha_send_event(
self._cluster.server_commands.get(IAS_ACE_BYPASS)[NAME],
{"zone_list": zone_list, "code": code},
)
"""

def _emergency(self) -> asyncio.Future:
"""Handle the IAS ACE emergency command."""
Expand Down
16 changes: 7 additions & 9 deletions zhaws/server/zigbee/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
from zigpy.zcl.clusters.general import Groups
import zigpy.zdo.types as zdo_types

from zhaws.backports.enum import StrEnum
from zhaws.server.const import (
DEVICE,
EVENT,
EVENT_TYPE,
IEEE,
MESSAGE_TYPE,
NWK,
DeviceEvents,
EventTypes,
MessageTypes,
)
Expand Down Expand Up @@ -108,13 +108,6 @@ class DeviceStatus(Enum):
INITIALIZED = 2


class DeviceEvents(StrEnum):
"""Events that devices can broadcast."""

DEVICE_OFFLINE = "device_offline"
DEVICE_ONLINE = "device_online"


class Device(LogMixin):
"""ZHAWSS Zigbee device object."""

Expand Down Expand Up @@ -312,7 +305,10 @@ def device_automation_triggers(self) -> dict:
if hasattr(self._zigpy_device, "device_automation_triggers"):
triggers.update(self._zigpy_device.device_automation_triggers)

return triggers
return_triggers = {
f"{key[0]}~{key[1]}": value for key, value in triggers.items()
}
return return_triggers

@property
def available(self) -> bool:
Expand Down Expand Up @@ -574,6 +570,8 @@ def zha_device_info(self) -> dict:
)
device_info[ATTR_ENDPOINT_NAMES] = names

device_info["device_automation_triggers"] = self.device_automation_triggers

return device_info

def async_get_clusters(self) -> dict[int, dict[CLUSTER_TYPE, list[int]]]:
Expand Down
14 changes: 1 addition & 13 deletions zhaws/server/zigbee/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ def send_event(self, signal: dict[str, Any]) -> None:
"""Broadcast an event from this endpoint."""
signal["endpoint"] = {
"id": self.id,
"unique_id": self.unique_id,
}
# signal["endpoint_id"] = self.id
self.device.send_event(signal)

def claim_cluster_handlers(self, cluster_handlers: list[ClusterHandler]) -> None:
Expand All @@ -218,15 +218,3 @@ def unclaimed_cluster_handlers(self) -> list[ClusterHandler]:
self.all_cluster_handlers[cluster_id]
for cluster_id in (available - claimed)
]

""" TODO
def zha_send_event(self, event_data: dict[str, str | int]) -> None:
#Relay events to hass.
self._channels.zha_send_event(
{
const.ATTR_UNIQUE_ID: self.unique_id,
const.ATTR_ENDPOINT_ID: self.id,
**event_data,
}
)
"""

0 comments on commit 40d586f

Please sign in to comment.