Skip to content

Commit 049182a

Browse files
Implement push notifications for gateway (#1459)
* gateway push server * Add docs to implement a gateway zigbee device * Update miio/gateway/devices/subdevice.py Co-authored-by: Teemu R. <[email protected]> * Update miio/gateway/devices/subdevice.py Co-authored-by: Teemu R. <[email protected]> * Update docs/push_server.rst Co-authored-by: Teemu R. <[email protected]> * Update docs/push_server.rst Co-authored-by: Teemu R. <[email protected]> * Update miio/gateway/gateway.py Co-authored-by: Teemu R. <[email protected]> * Update miio/gateway/gateway.py Co-authored-by: Teemu R. <[email protected]> * Update docs/push_server.rst Co-authored-by: Teemu R. <[email protected]> * Update miio/gateway/alarm.py Co-authored-by: Teemu R. <[email protected]> * Update miio/gateway/devices/subdevice.py Co-authored-by: Teemu R. <[email protected]> * Add callback type hints * use typing instead of collections.abc * add type hints * Create gateway.rts * move gateway docs * add enter * raise Exception when no push server * fix flake8 * Fix documentation * Renamed the file to use correct extension * Fixed the link to obtaining event information * Marked code block to use yaml Co-authored-by: Teemu R. <[email protected]>
1 parent 5321c58 commit 049182a

File tree

8 files changed

+576
-9
lines changed

8 files changed

+576
-9
lines changed

docs/device_docs/gateway.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Gateway
2+
=======
3+
4+
Adding support for new Zigbee devices
5+
-------------------------------------
6+
7+
Once the event information is obtained as :ref:`described in the push server docs<obtain_event_info>`,
8+
a new event for a Zigbee device connected to a gateway can be implemented as follows:
9+
10+
1. Open `miio/gateway/devices/subdevices.yaml` file and search for the target device for the new event.
11+
2. Add an entry for the new event:
12+
13+
.. code-block:: yaml
14+
15+
properties:
16+
- property: is_open # the new property of this device (optional)
17+
default: False # default value of the property when the device is initialized (optional)
18+
push_properties:
19+
open: # the event you added, see the decoded packet capture `\"key\":\"event.lumi.sensor_magnet.aq2.open\"` take this equal to everything after the model
20+
property: is_open # the property as listed above that this event will link to (optional)
21+
value: True # the value the property as listed above will be set to if this event is received (optional)
22+
extra: "[1,6,1,0,[0,1],2,0]" # the identification of this event, see the decoded packet capture `\"extra\":\"[1,6,1,0,[0,1],2,0]\"`
23+
close:
24+
property: is_open
25+
value: False
26+
extra: "[1,6,1,0,[0,0],2,0]"
27+
28+
3. Create a pull request to get the event added to this library.

miio/gateway/alarm.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Xiaomi Gateway Alarm implementation."""
22

3+
import logging
34
from datetime import datetime
45

6+
from ..exceptions import DeviceException
7+
from ..push_server import EventInfo
58
from .gatewaydevice import GatewayDevice
69

10+
_LOGGER = logging.getLogger(__name__)
11+
712

813
class Alarm(GatewayDevice):
914
"""Class representing the Xiaomi Gateway Alarm."""
@@ -61,3 +66,29 @@ def set_triggering_volume(self, volume):
6166
def last_status_change_time(self) -> datetime:
6267
"""Return the last time the alarm changed status."""
6368
return datetime.fromtimestamp(self._gateway.send("get_arming_time").pop())
69+
70+
def subscribe_events(self):
71+
"""subscribe to the alarm events using the push server."""
72+
if self._gateway._push_server is None:
73+
raise DeviceException(
74+
"Can not install push callback without a PushServer instance"
75+
)
76+
77+
event_info = EventInfo(
78+
action="alarm_triggering",
79+
extra="[1,19,1,111,[0,1],2,0]",
80+
trigger_token=self._gateway.token,
81+
)
82+
83+
event_id = self._gateway._push_server.subscribe_event(self._gateway, event_info)
84+
if event_id is None:
85+
return False
86+
87+
self._event_ids.append(event_id)
88+
return True
89+
90+
def unsubscribe_events(self):
91+
"""Unsubscibe from events registered in the gateway memory."""
92+
for event_id in self._event_ids:
93+
self._gateway._push_server.unsubscribe_event(self._gateway, event_id)
94+
self._event_ids.remove(event_id)

miio/gateway/devices/subdevice.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
"""Xiaomi Gateway subdevice base class."""
22

33
import logging
4-
from typing import TYPE_CHECKING, Dict, Optional
4+
from typing import TYPE_CHECKING, Dict, List, Optional
55

66
import attr
77
import click
88

99
from ...click_common import command
10-
from ..gateway import GATEWAY_MODEL_EU, GATEWAY_MODEL_ZIG3, GatewayException
10+
from ...exceptions import DeviceException
11+
from ...push_server import EventInfo
12+
from ..gateway import (
13+
GATEWAY_MODEL_EU,
14+
GATEWAY_MODEL_ZIG3,
15+
GatewayCallback,
16+
GatewayException,
17+
)
1118

1219
_LOGGER = logging.getLogger(__name__)
1320
if TYPE_CHECKING:
@@ -60,6 +67,10 @@ def __init__(
6067

6168
self.setter = model_info.get("setter")
6269

70+
self.push_events = model_info.get("push_properties", [])
71+
self._event_ids: List[str] = []
72+
self._registered_callbacks: Dict[str, GatewayCallback] = {}
73+
6374
def __repr__(self):
6475
return "<Subdevice {}: {}, model: {}, zigbee: {}, fw: {}, bat: {}, vol: {}, props: {}>".format(
6576
self.device_type,
@@ -260,3 +271,69 @@ def get_firmware_version(self) -> Optional[int]:
260271
ex,
261272
)
262273
return self._fw_ver
274+
275+
def register_callback(self, id: str, callback: GatewayCallback):
276+
"""Register a external callback function for updates of this subdevice."""
277+
if id in self._registered_callbacks:
278+
_LOGGER.error(
279+
"A callback with id '%s' was already registed, overwriting previous callback",
280+
id,
281+
)
282+
self._registered_callbacks[id] = callback
283+
284+
def remove_callback(self, id: str):
285+
"""Remove a external callback using its id."""
286+
self._registered_callbacks.pop(id)
287+
288+
def push_callback(self, action: str, params: str):
289+
"""Push callback received from the push server."""
290+
if action not in self.push_events:
291+
_LOGGER.error(
292+
"Received unregistered action '%s' callback for sid '%s' model '%s'",
293+
action,
294+
self.sid,
295+
self.model,
296+
)
297+
298+
event = self.push_events[action]
299+
prop = event.get("property")
300+
value = event.get("value")
301+
if prop is not None and value is not None:
302+
self._props[prop] = value
303+
304+
for callback in self._registered_callbacks.values():
305+
callback(action, params)
306+
307+
def subscribe_events(self):
308+
"""subscribe to all subdevice events using the push server."""
309+
if self._gw._push_server is None:
310+
raise DeviceException(
311+
"Can not install push callback without a PushServer instance"
312+
)
313+
314+
result = True
315+
for action in self.push_events:
316+
event_info = EventInfo(
317+
action=action,
318+
extra=self.push_events[action]["extra"],
319+
source_sid=self.sid,
320+
source_model=self.zigbee_model,
321+
event=self.push_events[action].get("event", None),
322+
command_extra=self.push_events[action].get("command_extra", ""),
323+
trigger_value=self.push_events[action].get("trigger_value"),
324+
)
325+
326+
event_id = self._gw._push_server.subscribe_event(self._gw, event_info)
327+
if event_id is None:
328+
result = False
329+
continue
330+
331+
self._event_ids.append(event_id)
332+
333+
return result
334+
335+
def unsubscribe_events(self):
336+
"""Unsubscibe from events registered in the gateway memory."""
337+
for event_id in self._event_ids:
338+
self._gw._push_server.unsubscribe_event(self._gw, event_id)
339+
self._event_ids.remove(event_id)

0 commit comments

Comments
 (0)