Skip to content

Commit

Permalink
feat(z2m_integration): add listen_to attribute to choose between HA…
Browse files Browse the repository at this point in the history
… state and MQTT

related to #109
  • Loading branch information
xaviml committed Aug 8, 2020
1 parent 9807211 commit 0274db5
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 4 deletions.
2 changes: 1 addition & 1 deletion apps/controllerx/cx_core/controller.py
Expand Up @@ -178,7 +178,7 @@ async def handle_action(self, action_key: str) -> None:
else:
self.log(
f"🎮 Button event triggered, but not registered: `{action_key}`",
level="INFO",
level="DEBUG",
ascii_encode=False,
)

Expand Down
35 changes: 34 additions & 1 deletion apps/controllerx/cx_core/integration/z2m.py
@@ -1,10 +1,15 @@
import json
from typing import Optional

from appdaemon.plugins.hass.hassapi import Hass # type: ignore
from appdaemon.plugins.mqtt.mqttapi import Mqtt # type: ignore

from cx_const import TypeActionsMapping
from cx_core.integration import Integration

LISTENS_TO_HA = "ha"
LISTENS_TO_MQTT = "mqtt"


class Z2MIntegration(Integration):
def get_name(self) -> str:
Expand All @@ -14,7 +19,35 @@ def get_actions_mapping(self) -> Optional[TypeActionsMapping]:
return self.controller.get_z2m_actions_mapping()

def listen_changes(self, controller_id: str) -> None:
Hass.listen_state(self.controller, self.state_callback, controller_id)
listens_to = self.kwargs.get("listen_to", LISTENS_TO_HA)
if listens_to == LISTENS_TO_HA:
Hass.listen_state(self.controller, self.state_callback, controller_id)
elif listens_to == LISTENS_TO_MQTT:
Mqtt.listen_event(
self.controller,
self.event_callback,
topic=f"zigbee2mqtt/{controller_id}",
namespace="mqtt",
)
else:
raise ValueError(
"`listen_to` has to be either `ha` or `mqtt`. Default is `ha`."
)

async def event_callback(self, event_name: str, data: dict, kwargs: dict) -> None:
self.controller.log(f"MQTT data event: {data}", level="DEBUG")
action_key = self.kwargs.get("action_key", "action")
if "payload" not in data:
return
payload = json.loads(data["payload"])
if action_key not in data["payload"]:
self.controller.log(
f"⚠️ There is no `{action_key}` in the MQTT topic payload",
level="WARNING",
ascii_encode=False,
)
return
await self.controller.handle_action(payload[action_key])

async def state_callback(
self, entity: Optional[str], attribute: Optional[str], old, new, kwargs
Expand Down
31 changes: 30 additions & 1 deletion docs/others/integrations.md
Expand Up @@ -25,7 +25,36 @@ example_app:

#### Zigbee2MQTT

This integration(**`z2m`**) is meant to be used for zigbee2mqtt. It listens the states from the HA sensor entities. It does not have any additional arguments.
This integration(**`z2m`**) is meant to be used for zigbee2mqtt. It listens the states from the HA sensor entities. You can add `listen_to` attribute to indicate if it listens for HA states (`ha`) or MQTT topics (`mqtt`). Default is `ha`. If you want to use the `mqtt`, then you will need to change the `appdaemon.yaml` as it is stated in the `MQTT` integration section. Imagine we have the following configuration already created for a `z2m` controller listening to HA state:

```yaml
livingroom_controller:
module: controllerx
class: E1810Controller
controller: sensor.livingroom_controller_action
integration: z2m
light: light.bedroom
```

Then, if we want to listen to the MQTT topic directly (skipping the HA state machine), we will need to change to:

```yaml
livingroom_controller:
module: controllerx
class: E1810Controller
controller: livingroom_controller
integration:
name: z2m
listen_to: mqtt
action_key: action # By default is `action` already
light: light.bedroom
```

Three things to clarify when using the `z2m` integration listening to MQTT:

- `appdaemon.yaml` needs to be changed by adding the MQTT plugin (see `MQTT` section below).
- The Zigbee2MQTT friendly name from the z2m needs to be specified in the `controller` attribute.
- `action_key` is the key inside the topic payload that contains the fired action from the controller. It is normally `action` or `click`. By default will be `action`.

#### deCONZ

Expand Down
2 changes: 1 addition & 1 deletion docs/start/type-configuration.md
Expand Up @@ -87,7 +87,7 @@ _\* Required fields_

## Switch controller

This allows you to control switches. It supports turning on/off and toggling
This allows you to control `switch` entities as well as `input_boolean` and `binary_sensor`. It supports turning on/off and toggling.

| key | type | value | description |
| ---------- | ------ | --------------------------------------- | ----------------------------------------------------- |
Expand Down
35 changes: 35 additions & 0 deletions tests/cx_core/integration/z2m_test.py
@@ -0,0 +1,35 @@
from typing import Any
import pytest

from cx_core.integration.z2m import Z2MIntegration


@pytest.mark.parametrize(
"data, action_key, handle_action_called, expected_called_with",
[
({"payload": '{"event_1": "action_1"}'}, "event_1", True, "action_1"),
({}, None, False, Any),
({"payload": '{"action": "action_1"}'}, None, True, "action_1"),
({"payload": '{"event_1": "action_1"}'}, "event_2", False, Any),
],
)
@pytest.mark.asyncio
async def test_event_callback(
fake_controller,
mocker,
data,
action_key,
handle_action_called,
expected_called_with,
):
handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
z2m_integration = Z2MIntegration(fake_controller, {})
z2m_integration.kwargs = (
{"action_key": action_key} if action_key is not None else {}
)
await z2m_integration.event_callback("test", data, {})

if handle_action_called:
handle_action_patch.assert_called_once_with(expected_called_with)
else:
handle_action_patch.assert_not_called()

0 comments on commit 0274db5

Please sign in to comment.