Skip to content

Commit

Permalink
Merge pull request #469 from xaviml/feat/shelly-integration
Browse files Browse the repository at this point in the history
feat(integration): add shelly and shellyforhass integrations
  • Loading branch information
xaviml authored May 18, 2022
2 parents 9e455b4 + 3df7832 commit a242b8f
Show file tree
Hide file tree
Showing 17 changed files with 274 additions and 6 deletions.
3 changes: 1 addition & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,5 @@
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"python.analysis.completeFunctionParens": true
}
}
5 changes: 5 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ PRERELEASE_NOTE
## :pencil2: Features

- Add following predefined actions `on_min_max_brightness`, `on_max_min_brightness`, `on_min_max_color_temp` and `on_max_min_color_temp`. [ #472 ] @sabaatworld
- Add [`shelly` integration](https://xaviml.github.io/controllerx/start/integrations/#shelly).
- Add [`shellyforhass` integration](https://xaviml.github.io/controllerx/start/integrations/#shellyforhass).

<!--
## :hammer: Fixes
Expand All @@ -29,3 +31,6 @@ PRERELEASE_NOTE
## :video_game: New devices

- [ROB2000070](https://xaviml.github.io/controllerx/controllers/ROB2000070) - add device with Z2M support. [ #482 ] @hrak
- [ShellyI3](https://xaviml.github.io/controllerx/controllers/ShellyI3) - add device with ShellyForHASS support. [ #441 ]
- [ShellyPlusI4](https://xaviml.github.io/controllerx/controllers/ShellyPlusI4) - add device with Shelly support. [ #441 ]
- [Shelly25](https://xaviml.github.io/controllerx/controllers/Shelly25) - add device with Shelly support. [ #441 ]
1 change: 1 addition & 0 deletions apps/controllerx/controllerx.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from cx_devices.rgb_genie import *
from cx_devices.robb import *
from cx_devices.sengled import *
from cx_devices.shelly import *
from cx_devices.smartthings import *
from cx_devices.sonoff import *
from cx_devices.terncy import *
Expand Down
16 changes: 16 additions & 0 deletions apps/controllerx/cx_core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,5 +586,21 @@ def get_homematic_actions_mapping(self) -> Optional[DefaultActionsMapping]:
"""
return None

def get_shelly_actions_mapping(self) -> Optional[DefaultActionsMapping]:
"""
Controllers can implement this function. It should return a dict
with the command that a controller can take and the functions as values.
This is used for Shelly support.
"""
return None

def get_shellyforhass_actions_mapping(self) -> Optional[DefaultActionsMapping]:
"""
Controllers can implement this function. It should return a dict
with the command that a controller can take and the functions as values.
This is used for Shelly for HASS support.
"""
return None

def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
return {}
25 changes: 25 additions & 0 deletions apps/controllerx/cx_core/integration/shelly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Any, Dict, Optional

from appdaemon.plugins.hass.hassapi import Hass
from cx_const import DefaultActionsMapping
from cx_core.integration import EventData, Integration


class ShellyIntegration(Integration):
name = "shelly"

def get_default_actions_mapping(self) -> Optional[DefaultActionsMapping]:
return self.controller.get_shelly_actions_mapping()

async def listen_changes(self, controller_id: str) -> None:
await Hass.listen_event(
self.controller, self.event_callback, "shelly.click", device=controller_id
)

async def event_callback(
self, event_name: str, data: EventData, kwargs: Dict[str, Any]
) -> None:
click_type = data["click_type"]
channel = data["channel"]
action = f"{click_type}_{channel}"
await self.controller.handle_action(action, extra=data)
27 changes: 27 additions & 0 deletions apps/controllerx/cx_core/integration/shellyforhass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Any, Dict, Optional

from appdaemon.plugins.hass.hassapi import Hass
from cx_const import DefaultActionsMapping
from cx_core.integration import EventData, Integration


class ShellyForHASSIntegration(Integration):
name = "shellyforhass"

def get_default_actions_mapping(self) -> Optional[DefaultActionsMapping]:
return self.controller.get_shellyforhass_actions_mapping()

async def listen_changes(self, controller_id: str) -> None:
await Hass.listen_event(
self.controller,
self.event_callback,
"shellyforhass.click",
entity_id=controller_id,
)

async def event_callback(
self, event_name: str, data: EventData, kwargs: Dict[str, Any]
) -> None:
click_type = data["click_type"]
action = f"{click_type}"
await self.controller.handle_action(action, extra=data)
47 changes: 47 additions & 0 deletions apps/controllerx/cx_devices/shelly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from cx_const import DefaultActionsMapping, Light
from cx_core import LightController


class ShellyI3LightController(LightController):
def get_shellyforhass_actions_mapping(self) -> DefaultActionsMapping:
return {
"single": Light.CLICK_BRIGHTNESS_UP,
"long": Light.CLICK_BRIGHTNESS_DOWN,
"double": Light.ON_FULL_BRIGHTNESS,
}


class ShellyPlusI4LightController(LightController):
def get_shelly_actions_mapping(self) -> DefaultActionsMapping:
return {
"single_push_1": Light.ON,
"long_push_1": Light.HOLD_COLOR_UP,
"btn_up_1": Light.RELEASE,
"double_push_1": Light.ON_FULL_COLOR_TEMP,
"single_push_2": Light.OFF,
"long_push_2": Light.HOLD_COLOR_DOWN,
"btn_up_2": Light.RELEASE,
"double_push_2": Light.ON_MIN_COLOR_TEMP,
"single_push_3": Light.CLICK_BRIGHTNESS_UP,
"long_push_3": Light.HOLD_BRIGHTNESS_UP,
"btn_up_3": Light.RELEASE,
"double_push_3": Light.ON_FULL_BRIGHTNESS,
"single_push_4": Light.CLICK_BRIGHTNESS_DOWN,
"long_push_4": Light.HOLD_BRIGHTNESS_DOWN,
"btn_up_4": Light.RELEASE,
"double_push_4": Light.ON_MIN_BRIGHTNESS,
}


class Shelly25LightController(LightController):
def get_shelly_actions_mapping(self) -> DefaultActionsMapping:
return {
"single_push_1": Light.ON,
"long_push_1": Light.HOLD_BRIGHTNESS_UP,
"btn_up_1": Light.RELEASE,
"double_push_1": Light.ON_FULL_BRIGHTNESS,
"single_push_2": Light.OFF,
"long_push_2": Light.HOLD_BRIGHTNESS_DOWN,
"btn_up_2": Light.RELEASE,
"double_push_2": Light.ON_MIN_BRIGHTNESS,
}
Binary file added docs/docs/assets/controllers/Shelly25.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/docs/assets/controllers/ShellyI3.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/docs/assets/controllers/ShellyPlusI4.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions docs/docs/others/extract-controller-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ The name you need to add to the `controller` parameter can be found in `Configur

#### deCONZ

In case of deCONZ, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `deconz_event` and start listening. Then press any button and you will see event of the button, you will need to copy the `id` inside the `data` object.
In case of deCONZ, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `deconz_event` and start listening. Then, press any button and you will see event of the button, you will need to copy the `id` inside the `data` object.

#### ZHA

In case of ZHA, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `zha_event` and start listening. Then press any button and you will see event of the button, you will need to copy the `device_ieee` inside the `data` object. It is a number like the following 00:67:88:56:06:78:9b:3f.
In case of ZHA, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `zha_event` and start listening. Then, press any button and you will see event of the button, you will need to copy the `device_ieee` inside the `data` object. It is a number like the following 00:67:88:56:06:78:9b:3f.

#### MQTT

In case of using MQTT integration, the `controller` attribute must have the MQTT topic to listen from. It is important that the topic payload contains directly the action name and not a JSON. This means that in case of using the MQTT integration with a z2m controller, then the topic to listen to must be `zigbee2mqtt/<friendly name>/action` or `zigbee2mqtt/<friendly name>/click`. You can see the topic on the Zigbee2MQTT logs.

#### Homematic

In case of Homematic, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `homematic.keypress` and start listening. Then press any button and you will see event of the button, you will need to copy the `name` inside the `data` object.
In case of Homematic, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `homematic.keypress` and start listening. Then, press any button and you will see event of the button, you will need to copy the `name` inside the `data` object.

#### Shelly

In case of Shelly, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `shelly.click` and start listening. Then, press any button and you will see event of the button, you will need to copy the `device` inside the `data` object. You can read more about the event [here](https://www.home-assistant.io/integrations/shelly/#events).

#### ShellyForHASS

In case of ShellyForHASS, you can go to `Developer Tools > Events` then down the bottom you can subscribe for `shellyforhass.click` and start listening. Then, press any button and you will see event of the button, you will need to copy the `entity_id` inside the `data` object. You can read more about the event [here](https://github.com/StyraHem/ShellyForHASS#shellyforhassclick-020).
10 changes: 9 additions & 1 deletion docs/docs/start/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,12 @@ This integration(**`lutron_caseta`**) listens to `lutron_caseta_button_event` ev

#### Homematic

This integration ([**`homematic`**](https://www.home-assistant.io/integrations/homematic/)) listens to `homematic.keypress` events. It created an action like `<action_type>_<channel>`. It does not have any additional arguments.
This integration ([**`homematic`**](https://www.home-assistant.io/integrations/homematic)) listens to `homematic.keypress` events. It creates an action like `<action_type>_<channel>`. It does not have any additional arguments.

#### Shelly

This integration ([**`shelly`**](https://www.home-assistant.io/integrations/shelly)) listens to `shelly.click` events. It creates an action like `<click_type>_<channel>`. It does not have any additional arguments.

#### Shelly for HASS

This integration ([**`shellyforhass`**](https://github.com/StyraHem/ShellyForHASS)) listens to `shellyforhass.click` events. It creates an action like `<action_type>`. It does not have any additional arguments.
10 changes: 10 additions & 0 deletions docs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"state": "State",
"lutron": "Lutron Caseta",
"homematic": "Homematic",
"shelly": "Shelly",
"shellyforhass": "ShellyForHass",
}

INTEGRATIONS_EXAMPLES: List[Dict[str, Any]] = [
Expand All @@ -47,6 +49,12 @@
},
{"name": "lutron", "title": "Lutron Caseta", "controller": "87654321"},
{"name": "homematic", "title": "Homematic", "controller": "my_controller"},
{"name": "shelly", "title": "Shelly", "controller": "shellybutton-ABC123456"},
{
"name": "shellyforhass",
"title": "ShellyForHass",
"controller": "binary_sensor.shelly_button_switch",
},
]


Expand Down Expand Up @@ -168,6 +176,8 @@ def get_controller_docs(controller: TypeController[Entity]) -> ControllerDocs:
"state": controller.get_state_actions_mapping,
"lutron": controller.get_lutron_caseta_actions_mapping,
"homematic": controller.get_homematic_actions_mapping,
"shelly": controller.get_shelly_actions_mapping,
"shellyforhass": controller.get_shellyforhass_actions_mapping,
}
for integration, integration_mapping_func in integration_mappings_funcs.items():
mapping = integration_mapping_func()
Expand Down
2 changes: 2 additions & 0 deletions tests/unit_tests/cx_core/integration/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ def test_get_integrations(fake_controller: Controller) -> None:
"mqtt",
"lutron_caseta",
"homematic",
"shelly",
"shellyforhass",
}
62 changes: 62 additions & 0 deletions tests/unit_tests/cx_core/integration/shelly_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from typing import Any, Dict

import pytest
from appdaemon.plugins.hass.hassapi import Hass
from cx_core.controller import Controller
from cx_core.integration.shelly import ShellyIntegration
from pytest_mock.plugin import MockerFixture


@pytest.mark.parametrize(
"data, expected",
[
(
{
"device_id": "e09c64a22553484d804353ef97f6fcd6",
"device": "shellybutton1-A4C12A45174",
"channel": 1,
"click_type": "single",
"generation": 1,
},
"single_1",
),
(
{
"device_id": "e09d64a22553384d8043532f97f6fcd6",
"device": "shellybutton1-A4C13B45274",
"channel": 3,
"click_type": "btn_down",
"generation": 1,
},
"btn_down_3",
),
],
)
async def test_callback(
fake_controller: Controller,
mocker: MockerFixture,
data: Dict[str, Any],
expected: str,
) -> None:
handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
shelly_integration = ShellyIntegration(fake_controller, {})
await shelly_integration.event_callback("test", data, {})
handle_action_patch.assert_called_once_with(expected, extra=data)


async def test_listen_changes(
fake_controller: Controller,
mocker: MockerFixture,
) -> None:
controller_id = "controller_id"
listen_event_mock = mocker.patch.object(Hass, "listen_event")
shelly_integration = ShellyIntegration(fake_controller, {})

await shelly_integration.listen_changes(controller_id)

listen_event_mock.assert_called_once_with(
fake_controller,
shelly_integration.event_callback,
"shelly.click",
device=controller_id,
)
56 changes: 56 additions & 0 deletions tests/unit_tests/cx_core/integration/shellyforhass_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Any, Dict

import pytest
from appdaemon.plugins.hass.hassapi import Hass
from cx_core.controller import Controller
from cx_core.integration.shellyforhass import ShellyForHASSIntegration
from pytest_mock.plugin import MockerFixture


@pytest.mark.parametrize(
"data, expected",
[
(
{
"entity_id": "binary_sensor.shelly_shbtn_1_xxxxxx_switch",
"click_type": "single",
},
"single",
),
(
{
"entity_id": "binary_sensor.shelly_shbtn_1_xxxxxx_switch",
"click_type": "double",
},
"double",
),
],
)
async def test_callback(
fake_controller: Controller,
mocker: MockerFixture,
data: Dict[str, Any],
expected: str,
) -> None:
handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
shellyforhass_integration = ShellyForHASSIntegration(fake_controller, {})
await shellyforhass_integration.event_callback("test", data, {})
handle_action_patch.assert_called_once_with(expected, extra=data)


async def test_listen_changes(
fake_controller: Controller,
mocker: MockerFixture,
) -> None:
controller_id = "controller_id"
listen_event_mock = mocker.patch.object(Hass, "listen_event")
shellyforhass_integration = ShellyForHASSIntegration(fake_controller, {})

await shellyforhass_integration.listen_changes(controller_id)

listen_event_mock.assert_called_once_with(
fake_controller,
shellyforhass_integration.event_callback,
"shellyforhass.click",
entity_id=controller_id,
)
2 changes: 2 additions & 0 deletions tests/unit_tests/cx_devices/devices_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def test_devices(device_class: Type[Controller]) -> None:
device.get_lutron_caseta_actions_mapping,
device.get_state_actions_mapping,
device.get_homematic_actions_mapping,
device.get_shelly_actions_mapping,
device.get_shellyforhass_actions_mapping,
]
for func in integration_mappings_funcs:
mappings = func()
Expand Down

0 comments on commit a242b8f

Please sign in to comment.