diff --git a/.cz.toml b/.cz.toml
index 1191704b..7b91cdf6 100644
--- a/.cz.toml
+++ b/.cz.toml
@@ -1,6 +1,6 @@
[tool.commitizen]
name = "cz_conventional_commits"
-version = "4.6.0"
+version = "4.7.0"
tag_format = "v$major.$minor.$patch$prerelease"
version_files = [
"apps/controllerx/cx_version.py",
diff --git a/.github/ISSUE_TEMPLATE/new_device.md b/.github/ISSUE_TEMPLATE/new_device.md
index 8532480d..516b085b 100644
--- a/.github/ISSUE_TEMPLATE/new_device.md
+++ b/.github/ISSUE_TEMPLATE/new_device.md
@@ -12,24 +12,23 @@ assignees: xaviml
## Device Information
-* Device Model: [ eg. E1743 ]
-* Device Description: [ eg. IKEA TRADFRI E1743 wireless dimmer ]
-* Device Manufacturer: [ eg. IKEA ]
+- Device Model: [ eg. E1743 ]
+- Device Description: [ eg. IKEA TRADFRI E1743 wireless dimmer ]
+- Device Manufacturer: [ eg. IKEA ]
## Integrations
-If possible, provide the event mappings for the different actions that can be performed on the controller. Specify the integration.
+
### Integration: [ Choose from `z2m | deconz | zha` ]
#### Actions
-* `button_xyz_press`: Sent when button xyz is pressed
-* `button_xyz_hold`: Sent when button xyz is held
+- `button_xyz_press`: Sent when button xyz is pressed
+- `button_xyz_hold`: Sent when button xyz is held
#### Notes
-(Optional) Additional notes for the integration, eg. known bugs, issues or limitations of the device for the specified integration.
-
+
diff --git a/Pipfile b/Pipfile
index 82520269..1e1b695f 100644
--- a/Pipfile
+++ b/Pipfile
@@ -12,8 +12,8 @@ pytest-mock = "==3.5.1"
pytest-timeout = "==1.4.2"
mock = "==4.0.3"
pre-commit = "==2.10.1"
-commitizen = "==2.14.2"
-mypy = "==0.800"
+commitizen = "==2.15.2"
+mypy = "==0.812"
flake8 = "==3.8.4"
isort = "==5.7.0"
controllerx = {path = ".", editable = true}
diff --git a/README.md b/README.md
index 40a04d94..1e0d2853 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,6 @@
[![last-release](https://img.shields.io/github/v/release/xaviml/controllerx.svg?style=for-the-badge)](https://github.com/xaviml/controllerx/releases)
[![downloads-latest](https://img.shields.io/github/downloads/xaviml/controllerx/latest/total?style=for-the-badge)](http://github.com/xaviml/controllerx/releases/latest)
[![azure-pipelines-coverage](https://img.shields.io/azure-devops/coverage/xaviml93/ControllerX/1/main.svg?style=for-the-badge)](https://dev.azure.com/xaviml93/ControllerX/_build/latest?definitionId=1&branchName=main)
-[![Codacy Badge](https://img.shields.io/codacy/grade/542f29ab55a449099488601ec7400563/main?style=for-the-badge)](https://app.codacy.com/manual/xaviml/controllerx?utm_source=github.com&utm_medium=referral&utm_content=xaviml/controllerx&utm_campaign=Badge_Grade_Dashboard)
[![community-topic](https://img.shields.io/badge/community-topic-blue?style=for-the-badge)](https://community.home-assistant.io/t/controllerx-bring-full-functionality-to-light-and-media-player-controllers/148855)
[![buy-me-a-beer](https://img.shields.io/badge/sponsor-Buy%20me%20a%20beer-orange?style=for-the-badge)](https://www.buymeacoffee.com/xaviml)
diff --git a/apps/controllerx/cx_core/controller.py b/apps/controllerx/cx_core/controller.py
index 7c1daa0f..c2bfe4e4 100644
--- a/apps/controllerx/cx_core/controller.py
+++ b/apps/controllerx/cx_core/controller.py
@@ -488,5 +488,13 @@ def get_zha_action(self, data: EventData) -> Optional[str]:
"""
return None
+ def get_lutron_caseta_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 Lutron support.
+ """
+ return None
+
def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
return {}
diff --git a/apps/controllerx/cx_core/custom_controller.py b/apps/controllerx/cx_core/custom_controller.py
index 92f8b63c..a54143c3 100644
--- a/apps/controllerx/cx_core/custom_controller.py
+++ b/apps/controllerx/cx_core/custom_controller.py
@@ -12,7 +12,7 @@ async def init(self) -> None:
level="WARNING",
ascii_encode=False,
)
- await super().init()
+ await super().init() # pragma: no cover
class CustomMediaPlayerController(MediaPlayerController):
@@ -22,7 +22,7 @@ async def init(self) -> None:
level="WARNING",
ascii_encode=False,
)
- await super().init()
+ await super().init() # pragma: no cover
class CustomSwitchController(SwitchController):
@@ -32,7 +32,7 @@ async def init(self) -> None:
level="WARNING",
ascii_encode=False,
)
- await super().init()
+ await super().init() # pragma: no cover
class CustomCoverController(CoverController):
@@ -42,7 +42,7 @@ async def init(self) -> None:
level="WARNING",
ascii_encode=False,
)
- await super().init()
+ await super().init() # pragma: no cover
class CallServiceController(Controller):
@@ -52,4 +52,4 @@ async def init(self) -> None:
level="WARNING",
ascii_encode=False,
)
- await super().init()
+ await super().init() # pragma: no cover
diff --git a/apps/controllerx/cx_core/integration/lutron_caseta.py b/apps/controllerx/cx_core/integration/lutron_caseta.py
new file mode 100644
index 00000000..5a2592a3
--- /dev/null
+++ b/apps/controllerx/cx_core/integration/lutron_caseta.py
@@ -0,0 +1,26 @@
+from typing import Optional
+
+from appdaemon.plugins.hass.hassapi import Hass # type: ignore
+from cx_const import DefaultActionsMapping
+from cx_core.integration import EventData, Integration
+
+
+class LutronIntegration(Integration):
+ name = "lutron_caseta"
+
+ def get_default_actions_mapping(self) -> Optional[DefaultActionsMapping]:
+ return self.controller.get_lutron_caseta_actions_mapping()
+
+ def listen_changes(self, controller_id: str) -> None:
+ Hass.listen_event(
+ self.controller,
+ self.callback,
+ "lutron_caseta_button_event",
+ serial=controller_id,
+ )
+
+ async def callback(self, event_name: str, data: EventData, kwargs: dict) -> None:
+ button = data["button_number"]
+ action_type = data["action"]
+ action = f"button_{button}_{action_type}"
+ await self.controller.handle_action(action, extra=data)
diff --git a/apps/controllerx/cx_devices/ikea.py b/apps/controllerx/cx_devices/ikea.py
index 5687d8e2..c10ea693 100644
--- a/apps/controllerx/cx_devices/ikea.py
+++ b/apps/controllerx/cx_devices/ikea.py
@@ -486,3 +486,17 @@ def get_zha_actions_mapping(self) -> DefaultActionsMapping:
"down_close": Cover.TOGGLE_CLOSE,
"stop": Cover.STOP,
}
+
+
+class E1812LightController(LightController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "on": Light.TOGGLE,
+ "brightness_move_up": Light.HOLD_BRIGHTNESS_TOGGLE,
+ "brightness_stop": Light.RELEASE,
+ }
+
+
+class E1812SwitchController(SwitchController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
+ return {"on": Switch.TOGGLE}
diff --git a/apps/controllerx/cx_devices/lutron.py b/apps/controllerx/cx_devices/lutron.py
index 94b2e867..19e26b12 100644
--- a/apps/controllerx/cx_devices/lutron.py
+++ b/apps/controllerx/cx_devices/lutron.py
@@ -2,109 +2,153 @@
from cx_core import LightController, MediaPlayerController
-class LutronCasetaProPicoLightController(LightController):
- # This requires the LutronCasetaPro CUSTOM integration by upsert
- # https://github.com/upsert/lutron-caseta-pro
- # THIS WILL NOT WORK with the default Lutron Caseta integration
- # Pico remotes using this integration report 6 states from their sensor:
- # top button = "1", up button = "8", middle round = "2", down arrow = "16",
- # bottom button = "4", no button pressed = "0"
+class LZL4BWHL01LightController(LightController):
+ # Each button press fires an event but no separate
+ # hold event. Press of up or down generates a stop event
+ # when released.
+
+ def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ 1002: Light.ON_FULL_BRIGHTNESS,
+ 2001: Light.HOLD_BRIGHTNESS_UP,
+ 2003: Light.RELEASE,
+ 3001: Light.HOLD_BRIGHTNESS_DOWN,
+ 3003: Light.RELEASE,
+ 4002: Light.OFF,
+ }
+ def get_zha_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "move_to_level_with_on_off_254_4": Light.ON_FULL_BRIGHTNESS,
+ "step_with_on_off_0_30_6": Light.HOLD_BRIGHTNESS_UP,
+ "step_1_30_6": Light.HOLD_BRIGHTNESS_DOWN,
+ "move_to_level_with_on_off_0_4": Light.OFF,
+ "stop": Light.RELEASE,
+ }
+
+
+class Z31BRLLightController(LightController):
+ def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ 1002: Light.TOGGLE,
+ 2002: Light.CLICK_BRIGHTNESS_UP,
+ 3002: Light.CLICK_BRIGHTNESS_DOWN,
+ }
+
+
+class LutronPJ22BLightController(LightController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "1": Light.ON_FULL_BRIGHTNESS,
+ "4": Light.OFF,
+ }
+
+ def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "button_2_press": Light.ON,
+ "button_4_press": Light.OFF,
+ }
+
+
+class LutronPJ22BMediaPlayerController(MediaPlayerController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "1": MediaPlayer.PLAY_PAUSE,
+ "4": MediaPlayer.NEXT_TRACK,
+ }
+
+ def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "button_2_press": MediaPlayer.PLAY_PAUSE,
+ "button_4_press": MediaPlayer.NEXT_TRACK,
+ }
+
+
+class LutronPJ22BRLLightController(LightController):
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
return {
"1": Light.ON_FULL_BRIGHTNESS,
"8": Light.HOLD_BRIGHTNESS_UP,
- "2": Light.SET_HALF_BRIGHTNESS,
"16": Light.HOLD_BRIGHTNESS_DOWN,
"4": Light.OFF,
"0": Light.RELEASE,
}
-class LutronCasetaProPicoMediaPlayerController(MediaPlayerController):
- # This requires the LutronCasetaPro CUSTOM integration by upsert
- # https://github.com/upsert/lutron-caseta-pro
- # THIS WILL NOT WORK with the default Lutron Caseta integration
- # Pico remotes using this integration report 6 states from their sensor:
- # top button = "1", up button = "8", middle round = "2", down arrow = "16",
- # bottom button = "4", no button pressed = "0"
-
+class LutronPJ22BRLMediaPlayerController(MediaPlayerController):
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
return {
"1": MediaPlayer.PLAY_PAUSE,
"8": MediaPlayer.HOLD_VOLUME_UP,
- "2": MediaPlayer.NEXT_SOURCE,
"16": MediaPlayer.HOLD_VOLUME_DOWN,
"4": MediaPlayer.NEXT_TRACK,
"0": MediaPlayer.RELEASE,
}
-class LutronCasetaProPJ24BLightController(LightController):
- # This requires the LutronCasetaPro CUSTOM integration by upsert
- # https://github.com/upsert/lutron-caseta-pro
- # THIS WILL NOT WORK with the default Lutron Caseta integration
- # Pico remotes using this integration report 5 states from their sensor:
- # top button = "1", second button = "2", third button = "4",
- # bottom button = "8", no button pressed = "0"
-
+class LutronPJ23BRLLightController(LightController):
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
return {
"1": Light.ON_FULL_BRIGHTNESS,
- "2": Light.HOLD_BRIGHTNESS_UP,
- "4": Light.HOLD_BRIGHTNESS_DOWN,
- "8": Light.OFF,
+ "8": Light.HOLD_BRIGHTNESS_UP,
+ "2": Light.SET_HALF_BRIGHTNESS,
+ "16": Light.HOLD_BRIGHTNESS_DOWN,
+ "4": Light.OFF,
"0": Light.RELEASE,
}
+ def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "button_2_press": Light.ON_FULL_BRIGHTNESS,
+ "button_4_press": Light.OFF,
+ "button_3_press": Light.SET_HALF_BRIGHTNESS,
+ "button_5_press": Light.HOLD_BRIGHTNESS_UP,
+ "button_5_release": Light.RELEASE,
+ "button_6_press": Light.HOLD_BRIGHTNESS_DOWN,
+ "button_6_release": Light.RELEASE,
+ }
-class LutronCasetaProPJ24BMediaPlayerController(MediaPlayerController):
- # This requires the LutronCasetaPro CUSTOM integration by upsert
- # https://github.com/upsert/lutron-caseta-pro
- # THIS WILL NOT WORK with the default Lutron Caseta integration
- # Pico remotes using this integration report 5 states from their sensor:
- # top button = "1", second button = "2", third button = "4",
- # bottom button = "8", no button pressed = "0"
+class LutronPJ23BRLMediaPlayerController(MediaPlayerController):
def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
return {
"1": MediaPlayer.PLAY_PAUSE,
- "2": MediaPlayer.HOLD_VOLUME_UP,
- "4": MediaPlayer.HOLD_VOLUME_DOWN,
- "8": MediaPlayer.NEXT_TRACK,
+ "8": MediaPlayer.HOLD_VOLUME_UP,
+ "2": MediaPlayer.NEXT_SOURCE,
+ "16": MediaPlayer.HOLD_VOLUME_DOWN,
+ "4": MediaPlayer.NEXT_TRACK,
"0": MediaPlayer.RELEASE,
}
-
-class LZL4BWHL01LightController(LightController):
- # Each button press fires an event but no separate
- # hold event. Press of up or down generates a stop event
- # when released.
-
- def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
+ def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping:
return {
- 1002: Light.ON_FULL_BRIGHTNESS,
- 2001: Light.HOLD_BRIGHTNESS_UP,
- 2003: Light.RELEASE,
- 3001: Light.HOLD_BRIGHTNESS_DOWN,
- 3003: Light.RELEASE,
- 4002: Light.OFF,
+ "button_2_press": MediaPlayer.PLAY_PAUSE,
+ "button_4_press": MediaPlayer.NEXT_TRACK,
+ "button_3_press": MediaPlayer.NEXT_SOURCE,
+ "button_5_press": MediaPlayer.HOLD_VOLUME_UP,
+ "button_5_release": MediaPlayer.RELEASE,
+ "button_6_press": MediaPlayer.HOLD_VOLUME_DOWN,
+ "button_6_release": MediaPlayer.RELEASE,
}
- def get_zha_actions_mapping(self) -> DefaultActionsMapping:
+
+class LutronPJ24BLightController(LightController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
return {
- "move_to_level_with_on_off_254_4": Light.ON_FULL_BRIGHTNESS,
- "step_with_on_off_0_30_6": Light.HOLD_BRIGHTNESS_UP,
- "step_1_30_6": Light.HOLD_BRIGHTNESS_DOWN,
- "move_to_level_with_on_off_0_4": Light.OFF,
- "stop": Light.RELEASE,
+ "1": Light.ON_FULL_BRIGHTNESS,
+ "2": Light.HOLD_BRIGHTNESS_UP,
+ "4": Light.HOLD_BRIGHTNESS_DOWN,
+ "8": Light.OFF,
+ "0": Light.RELEASE,
}
-class Z31BRLLightController(LightController):
- def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
+class LutronPJ24BMediaPlayerController(MediaPlayerController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
return {
- 1002: Light.TOGGLE,
- 2002: Light.CLICK_BRIGHTNESS_UP,
- 3002: Light.CLICK_BRIGHTNESS_DOWN,
+ "1": MediaPlayer.PLAY_PAUSE,
+ "2": MediaPlayer.HOLD_VOLUME_UP,
+ "4": MediaPlayer.HOLD_VOLUME_DOWN,
+ "8": MediaPlayer.NEXT_TRACK,
+ "0": MediaPlayer.RELEASE,
}
diff --git a/apps/controllerx/cx_devices/muller_licht.py b/apps/controllerx/cx_devices/muller_licht.py
index 911bda3a..a6d721ed 100644
--- a/apps/controllerx/cx_devices/muller_licht.py
+++ b/apps/controllerx/cx_devices/muller_licht.py
@@ -1,5 +1,7 @@
from cx_const import DefaultActionsMapping, Light
from cx_core import LightController
+from cx_core.controller import Controller
+from cx_core.integration import EventData
class MLI404011LightController(LightController):
@@ -42,3 +44,39 @@ def get_deconz_actions_mapping(self) -> DefaultActionsMapping:
# 11002: "", # fire button
# 12002: "", # heart button
}
+
+
+class MLI404002Controller(Controller):
+ def get_zha_action(self, data: EventData) -> str:
+ command = data["command"]
+ if command not in ("move", "step"):
+ return command
+ args = data["args"]
+ direction_mapping = {0: "up", 1: "down"}
+ return f"{command}_{direction_mapping[args[0]]}"
+
+
+class MLI404002LightController(MLI404002Controller, LightController):
+ def get_z2m_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "on": Light.TOGGLE,
+ "off": Light.TOGGLE,
+ "brightness_step_up": Light.CLICK_BRIGHTNESS_UP,
+ "brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN,
+ "brightness_move_up": Light.HOLD_BRIGHTNESS_UP,
+ "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN,
+ "brightness_stop": Light.RELEASE,
+ "recall_1": Light.ON_FULL_BRIGHTNESS,
+ }
+
+ def get_zha_actions_mapping(self) -> DefaultActionsMapping:
+ return {
+ "on": Light.TOGGLE,
+ "off": Light.TOGGLE,
+ "move_up": Light.HOLD_BRIGHTNESS_UP,
+ "move_down": Light.HOLD_BRIGHTNESS_DOWN,
+ "stop": Light.RELEASE,
+ "step_up": Light.CLICK_BRIGHTNESS_UP,
+ "step_down": Light.CLICK_BRIGHTNESS_DOWN,
+ "recall": Light.ON_FULL_BRIGHTNESS,
+ }
diff --git a/apps/controllerx/cx_version.py b/apps/controllerx/cx_version.py
index ac7979a2..a898594f 100644
--- a/apps/controllerx/cx_version.py
+++ b/apps/controllerx/cx_version.py
@@ -1 +1 @@
-__version__ = "v4.6.0"
+__version__ = "v4.7.0"
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 65880ae6..a6500b12 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -40,6 +40,9 @@ stages:
displayName: Lock dependencies
- script: pipenv install --system --deploy --dev
displayName: Install dependencies
+ - script: pip install dataclasses
+ displayName: Installing dataclasses
+ condition: eq(variables['python.version'], '3.6')
- script: isort apps/controllerx/ tests/ --check
displayName: Organize imports (isort)
- script: black apps/controllerx/ tests/ --check
diff --git a/docs/_data/controllers/E1812.yml b/docs/_data/controllers/E1812.yml
new file mode 100644
index 00000000..df8c976f
--- /dev/null
+++ b/docs/_data/controllers/E1812.yml
@@ -0,0 +1,21 @@
+name: E1812 (IKEA)
+device_support:
+ - type: Light
+ domain: light
+ controller: E1812LightController
+ delay: 350
+ mapping:
+ - "Click → Toggle"
+ - "Hold → Brightness up/down with direction changes"
+ - type: Switch
+ domain: switch
+ controller: E1812SwitchController
+ mapping:
+ - "Click → Toggle"
+integrations:
+ - name: Zigbee2MQTT
+ codename: z2m
+ actions:
+ - "on → Click"
+ - "brightness_move_up → Hold"
+ - "brightness_stop → Released after being held"
diff --git a/docs/_data/controllers/MLI-404002.yml b/docs/_data/controllers/MLI-404002.yml
new file mode 100644
index 00000000..76b4b7f8
--- /dev/null
+++ b/docs/_data/controllers/MLI-404002.yml
@@ -0,0 +1,37 @@
+name: MLI-404002 (Müller Licht)
+device_support:
+ - type: Light
+ domain: light
+ controller: MLI404002LightController
+ delay: 500
+ mapping:
+ - "Toggle button → Toggle"
+ - "Click 🔆 → Brighten up (1 step)"
+ - "Click 🔅 → Dim down (1 step)"
+ - "Click cold → Color temp down / Left color wheel (1 step) (not for z2m)"
+ - "Click warm → Color temp up / Right color wheel (1 step) (not for z2m)"
+ - "Hold 🔆 → Brighten up"
+ - "Hold 🔅→ Dim down"
+integrations:
+ - name: Zigbee2MQTT
+ codename: z2m
+ actions:
+ - '"on" → Toggle button'
+ - '"off" → Toggle button'
+ - brightness_step_down → Click 🔅
+ - brightness_move_down → Hold 🔅
+ - brightness_stop → Release 🔅/🔆
+ - brightness_step_up → Click 🔆
+ - brightness_move_up → Hold 🔆
+ - recall_1 → Click arrow back
+ - name: ZHA
+ codename: zha
+ actions:
+ - '"on" → Toggle button'
+ - '"off" → Toggle button'
+ - move_up → Hold 🔆
+ - move_down → Hold 🔅
+ - stop → Release 🔅/🔆
+ - step_up → Click 🔆
+ - step_down → Click 🔅
+ - recall → Click arrow back
diff --git a/docs/_data/controllers/MLI-404011.yml b/docs/_data/controllers/MLI-404011.yml
index 6491ff5d..b33c12e2 100644
--- a/docs/_data/controllers/MLI-404011.yml
+++ b/docs/_data/controllers/MLI-404011.yml
@@ -13,11 +13,11 @@ device_support:
- "Hold 🔆 → Brighten up"
- "Hold 🔅→ Dim down"
integrations:
- - name: deCONZ
- codename: deconz
+ - name: Zigbee2MQTT
+ codename: z2m
actions:
- - on → Toggle button
- - off → Toggle button
+ - '"on" → Toggle button'
+ - '"off" → Toggle button'
- brightness_down_click → Click 🔅
- brightness_down_hold → Hold 🔅
- brightness_down_release → Release 🔅
diff --git a/docs/_data/controllers/PJ2-2B.yml b/docs/_data/controllers/PJ2-2B.yml
index 43705c59..ec3626e9 100644
--- a/docs/_data/controllers/PJ2-2B.yml
+++ b/docs/_data/controllers/PJ2-2B.yml
@@ -2,20 +2,20 @@ name: PJ2-2B (Lutron Caseta Pro)
device_support:
- type: Light
domain: light
- controller: LutronCasetaProPicoLightController
+ controller: LutronPJ22BLightController
delay: 350
mapping:
- "Top button → Full brightness"
- "Bottom button → Turn off"
- type: Media Player
domain: media_player
- controller: LutronCasetaProPicoMediaPlayerController
+ controller: LutronPJ22BMediaPlayerController
delay: 500
mapping:
- "Top button → Play/Pause"
- "Bottom button → Next track"
note: >-
- This requires the LutronCasetaPro
+ For the State integration, it requires the LutronCasetaPro
CUSTOM integration by upsert. THIS WILL NOT WORK with the default Lutron Caseta integration.
All Lutron Caseta Pro Pico remotes supported by LutronCasetaPro are supported using this
controller type except for the 4-button PJ2-4B remotes which is separate.
@@ -24,6 +24,13 @@ integrations:
- name: State
codename: state
actions:
- - "\"1\" → Top button"
- - "\"4\" → Bottom button"
- - "\"0\" → Release/Idle state"
+ - '"1" → Top button'
+ - '"4" → Bottom button'
+ - '"0" → Release/Idle state'
+ - name: Lutron Caseta
+ codename: lutron_caseta
+ actions:
+ - button_2_press → Top button
+ - button_4_press → Bottom button
+ - button_2_release → Release top button
+ - button_4_release → Release bottom button
diff --git a/docs/_data/controllers/PJ2-2BRL.yml b/docs/_data/controllers/PJ2-2BRL.yml
index 67bbdcc3..d7a56b66 100644
--- a/docs/_data/controllers/PJ2-2BRL.yml
+++ b/docs/_data/controllers/PJ2-2BRL.yml
@@ -2,7 +2,7 @@ name: PJ2-2BRL (Lutron Caseta Pro)
device_support:
- type: Light
domain: light
- controller: LutronCasetaProPicoLightController
+ controller: LutronPJ22BRLLightController
delay: 350
mapping:
- "Top button → Full brightness"
@@ -11,7 +11,7 @@ device_support:
- "Bottom button → Turn off"
- type: Media Player
domain: media_player
- controller: LutronCasetaProPicoMediaPlayerController
+ controller: LutronPJ22BRLMediaPlayerController
delay: 500
mapping:
- "Top button → Play/Pause"
@@ -19,7 +19,7 @@ device_support:
- "Arrow down button → Volume down"
- "Bottom button → Next track"
note: >-
- This requires the LutronCasetaPro
+ For the State integration, it requires the LutronCasetaPro
CUSTOM integration by upsert. THIS WILL NOT WORK with the default Lutron Caseta integration.
All Lutron Caseta Pro Pico remotes supported by LutronCasetaPro are supported using this
controller type except for the 4-button PJ2-4B remotes which is separate.
@@ -28,8 +28,8 @@ integrations:
- name: State
codename: state
actions:
- - "\"1\" → Top button"
- - "\"8\" → Arrow up button"
- - "\"16\" → Arrow down button"
- - "\"4\" → Bottom button"
- - "\"0\" → Release/Idle state"
+ - '"1" → Top button'
+ - '"8" → Arrow up button'
+ - '"16" → Arrow down button'
+ - '"4" → Bottom button'
+ - '"0" → Release/Idle state'
diff --git a/docs/_data/controllers/PJ2-3BRL.yml b/docs/_data/controllers/PJ2-3BRL.yml
index 28f68381..d401bb07 100644
--- a/docs/_data/controllers/PJ2-3BRL.yml
+++ b/docs/_data/controllers/PJ2-3BRL.yml
@@ -2,7 +2,7 @@ name: PJ2-3BRL (Lutron Caseta Pro)
device_support:
- type: Light
domain: light
- controller: LutronCasetaProPicoLightController
+ controller: LutronPJ23BRLLightController
delay: 350
mapping:
- "Top button → Full brightness"
@@ -12,7 +12,7 @@ device_support:
- "Bottom button → Turn off"
- type: Media Player
domain: media_player
- controller: LutronCasetaProPicoMediaPlayerController
+ controller: LutronPJ23BRLMediaPlayerController
delay: 500
mapping:
- "Top button → Play/Pause"
@@ -21,7 +21,7 @@ device_support:
- "Arrow down button → Volume down"
- "Bottom button → Next track"
note: >-
- This requires the LutronCasetaPro
+ For the State integration, it requires the LutronCasetaPro
CUSTOM integration by upsert. THIS WILL NOT WORK with the default Lutron Caseta integration.
All Lutron Caseta Pro Pico remotes supported by LutronCasetaPro are supported using this
controller type except for the 4-button PJ2-4B remotes which is separate.
@@ -30,9 +30,22 @@ integrations:
- name: State
codename: state
actions:
- - "\"1\" → Top button"
- - "\"8\" → Arrow up button"
- - "\"2\" → Favourite/Middle round"
- - "\"16\" → Arrow down button"
- - "\"4\" → Bottom button"
- - "\"0\" → Release/Idle state"
+ - '"1" → Top button'
+ - '"8" → Arrow up button'
+ - '"2" → Favourite/Middle round'
+ - '"16" → Arrow down button'
+ - '"4" → Bottom button'
+ - '"0" → Release/Idle state'
+ - name: Lutron Caseta
+ codename: lutron_caseta
+ actions:
+ - button_2_press → Top button
+ - button_2_release → Release top button
+ - button_4_press → Bottom button
+ - button_4_release → Release bottom button
+ - button_3_press → Favourite/Middle round
+ - button_3_release → Release Favourite/Middle round
+ - button_5_press → Arrow up button
+ - button_5_release → Release arrow up button
+ - button_6_press → Arrow down button
+ - button_6_release → Release arrow down button
diff --git a/docs/_data/controllers/PJ2-4B.yml b/docs/_data/controllers/PJ2-4B.yml
index 7b8501f9..7e05b211 100644
--- a/docs/_data/controllers/PJ2-4B.yml
+++ b/docs/_data/controllers/PJ2-4B.yml
@@ -2,7 +2,7 @@ name: PJ2-4B (Lutron Caseta Pro)
device_support:
- type: Light
domain: light
- controller: LutronCasetaProPJ24BLightController
+ controller: LutronPJ24BLightController
delay: 350
mapping:
- "Top button → Full brightness"
@@ -11,7 +11,7 @@ device_support:
- "Bottom button → Turn off"
- type: Media Player
domain: media_player
- controller: LutronCasetaProPicoPJ24BMediaPlayerController
+ controller: LutronPJ24BMediaPlayerController
delay: 500
mapping:
- "Top button → Play/Pause"
@@ -19,15 +19,15 @@ device_support:
- "Third button → Volume down"
- "Bottom button → Next track"
note: >-
- This requires the LutronCasetaPro
+ For the State integration, it requires the LutronCasetaPro
CUSTOM integration by upsert. THIS WILL NOT WORK with the default Lutron Caseta integration.
integrations:
- name: State
codename: state
actions:
- - "\"1\" → Top button"
- - "\"2\" → Second button"
- - "\"4\" → Third button"
- - "\"8\" → Bottom button"
- - "\"0\" → Release/Idle state"
+ - '"1" → Top button'
+ - '"2" → Second button'
+ - '"4" → Third button'
+ - '"8" → Bottom button'
+ - '"0" → Release/Idle state'
diff --git a/docs/assets/img/E1812.jpeg b/docs/assets/img/E1812.jpeg
new file mode 100644
index 00000000..cf1dfefa
Binary files /dev/null and b/docs/assets/img/E1812.jpeg differ
diff --git a/docs/assets/img/MLI-404002.jpeg b/docs/assets/img/MLI-404002.jpeg
new file mode 100644
index 00000000..7d6de30b
Binary files /dev/null and b/docs/assets/img/MLI-404002.jpeg differ
diff --git a/docs/controllers/E1812.md b/docs/controllers/E1812.md
new file mode 100644
index 00000000..fd6d85eb
--- /dev/null
+++ b/docs/controllers/E1812.md
@@ -0,0 +1,5 @@
+---
+layout: controller
+title: E1812 (IKEA)
+device: E1812
+---
diff --git a/docs/controllers/MLI-404002.md b/docs/controllers/MLI-404002.md
new file mode 100644
index 00000000..ee6ac2b3
--- /dev/null
+++ b/docs/controllers/MLI-404002.md
@@ -0,0 +1,5 @@
+---
+layout: controller
+title: MLI-404002 (Müller Licht)
+device: MLI-404002
+---
diff --git a/docs/others/integrations.md b/docs/others/integrations.md
index f9ab2f17..c818a4a5 100644
--- a/docs/others/integrations.md
+++ b/docs/others/integrations.md
@@ -41,7 +41,7 @@ Three things to clarify when using the `z2m` integration listening to MQTT:
#### deCONZ
-This integration(**`deconz`**) listens to events and actions gets fired by default with the `event` attribute from the `data` object. However, you can change the attribute to listen to by adding a `type` attribute. This is an example
+This integration(**`deconz`**) listens to `deconz_event` events and actions gets fired by default with the `event` attribute from the `data` object. However, you can change the attribute to listen to by adding a `type` attribute. This is an example
```yaml
example_app:
@@ -56,7 +56,7 @@ example_app:
#### ZHA
-This integration(**`zha`**) listens to events and concatenates the command with the argument for the action string. It does not have any additional arguments.
+This integration(**`zha`**) listens to `zha_event` events and concatenates the command with the argument for the action string. It does not have any additional arguments.
#### MQTT
@@ -128,4 +128,8 @@ example_app:
mapping:
1_click: "on"
2_click: "off"
-```
\ No newline at end of file
+```
+
+#### Lutron Caséta
+
+This integration(**`lutron_caseta`**) listens to `lutron_caseta_button_event` events. It creates an action like `button__`. It does not have any additional arguments.
diff --git a/setup.cfg b/setup.cfg
index e45a824a..0e846d87 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -18,13 +18,14 @@ no_implicit_optional = True
mock_use_standalone_module = true
timeout = 5
-[report]
+[coverage:report]
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain if tests don't hit defensive assertion code:
raise NotImplementedError
+ self.log
[mypy-appdaemon.*]
ignore_missing_imports = true
diff --git a/tests/unit_tests/cx_core/custom_controller_test.py b/tests/unit_tests/cx_core/custom_controller_test.py
index e3c8931b..5fcd2f85 100644
--- a/tests/unit_tests/cx_core/custom_controller_test.py
+++ b/tests/unit_tests/cx_core/custom_controller_test.py
@@ -120,7 +120,7 @@ async def test_custom_controllers(
[("homeassistant/test_service2", {})],
),
(
- "z2m",
+ "lutron_caseta",
{"service": "homeassistant/test_service2"},
[("homeassistant/test_service2", {})],
),
diff --git a/tests/unit_tests/cx_core/integration/deconz_test.py b/tests/unit_tests/cx_core/integration/deconz_test.py
new file mode 100644
index 00000000..6c26c0a0
--- /dev/null
+++ b/tests/unit_tests/cx_core/integration/deconz_test.py
@@ -0,0 +1,38 @@
+from typing import Dict, Optional
+
+import pytest
+from cx_core.controller import Controller
+from cx_core.integration.deconz import DeCONZIntegration
+from pytest_mock.plugin import MockerFixture
+
+
+@pytest.mark.parametrize(
+ "data, type, expected",
+ [
+ (
+ {"id": 123, "event": 1002},
+ None,
+ 1002,
+ ),
+ (
+ {"id": 123, "gesture": 2},
+ "gesture",
+ 2,
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_callback(
+ fake_controller: Controller,
+ mocker: MockerFixture,
+ data: Dict,
+ type: Optional[str],
+ expected: str,
+):
+ handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
+ kwargs = {}
+ if type is not None:
+ kwargs["type"] = type
+ deconz_integration = DeCONZIntegration(fake_controller, kwargs)
+ await deconz_integration.event_callback("test", data, {})
+ handle_action_patch.assert_called_once_with(expected, extra=data)
diff --git a/tests/unit_tests/cx_core/integration/integration_test.py b/tests/unit_tests/cx_core/integration/integration_test.py
index 8f3f01c7..98ee2fa0 100644
--- a/tests/unit_tests/cx_core/integration/integration_test.py
+++ b/tests/unit_tests/cx_core/integration/integration_test.py
@@ -5,4 +5,11 @@
def test_get_integrations(fake_controller: Controller):
integrations = integration_module.get_integrations(fake_controller, {})
inteagration_names = {i.name for i in integrations}
- assert inteagration_names == {"z2m", "zha", "deconz", "state", "mqtt"}
+ assert inteagration_names == {
+ "z2m",
+ "zha",
+ "deconz",
+ "state",
+ "mqtt",
+ "lutron_caseta",
+ }
diff --git a/tests/unit_tests/cx_core/integration/lutron_test.py b/tests/unit_tests/cx_core/integration/lutron_test.py
new file mode 100644
index 00000000..fd284151
--- /dev/null
+++ b/tests/unit_tests/cx_core/integration/lutron_test.py
@@ -0,0 +1,46 @@
+from typing import Dict
+
+import pytest
+from cx_core.controller import Controller
+from cx_core.integration.lutron_caseta import LutronIntegration
+from pytest_mock.plugin import MockerFixture
+
+
+@pytest.mark.parametrize(
+ "data, expected",
+ [
+ (
+ {
+ "serial": 28786608,
+ "type": "FourGroupRemote",
+ "button_number": 4,
+ "device_name": "Shade Remote",
+ "area_name": "Upstairs Hall",
+ "action": "press",
+ },
+ "button_4_press",
+ ),
+ (
+ {
+ "serial": 28786608,
+ "type": "FourGroupRemote",
+ "button_number": 0,
+ "device_name": "Shade Remote",
+ "area_name": "Upstairs Hall",
+ "action": "hold",
+ },
+ "button_0_hold",
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_callback(
+ fake_controller: Controller,
+ mocker: MockerFixture,
+ data: Dict,
+ expected: str,
+):
+ handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
+ lutron_integration = LutronIntegration(fake_controller, {})
+ await lutron_integration.callback("test", data, {})
+ handle_action_patch.assert_called_once_with(expected, extra=data)
diff --git a/tests/unit_tests/cx_core/integration/mqtt_test.py b/tests/unit_tests/cx_core/integration/mqtt_test.py
new file mode 100644
index 00000000..e2dd3c18
--- /dev/null
+++ b/tests/unit_tests/cx_core/integration/mqtt_test.py
@@ -0,0 +1,36 @@
+from typing import Dict
+
+import pytest
+from cx_core.controller import Controller
+from cx_core.integration.mqtt import MQTTIntegration
+from pytest_mock.plugin import MockerFixture
+
+
+@pytest.mark.parametrize(
+ "data, expected",
+ [
+ (
+ {"payload": "click"},
+ "click",
+ ),
+ (
+ {},
+ None,
+ ),
+ ],
+)
+@pytest.mark.asyncio
+async def test_callback(
+ fake_controller: Controller,
+ mocker: MockerFixture,
+ data: Dict,
+ expected: str,
+):
+ handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
+ mqtt_integration = MQTTIntegration(fake_controller, {})
+ await mqtt_integration.event_callback("test", data, {})
+
+ if expected is not None:
+ handle_action_patch.assert_called_once_with(expected)
+ else:
+ handle_action_patch.assert_not_called()
diff --git a/tests/unit_tests/cx_core/integration/zha_test.py b/tests/unit_tests/cx_core/integration/zha_test.py
index 9ea588cb..ac86a817 100644
--- a/tests/unit_tests/cx_core/integration/zha_test.py
+++ b/tests/unit_tests/cx_core/integration/zha_test.py
@@ -1,4 +1,4 @@
-from typing import Dict
+from typing import Dict, Optional
import pytest
from cx_core.controller import Controller
@@ -38,12 +38,12 @@
],
)
@pytest.mark.asyncio
-async def test_get_integrations(
+async def test_callback(
fake_controller: Controller,
mocker: MockerFixture,
command: str,
args: Dict,
- expected_called_with: str,
+ expected_called_with: Optional[str],
):
data = {"command": command, "args": args}
handle_action_patch = mocker.patch.object(fake_controller, "handle_action")
diff --git a/tests/unit_tests/cx_devices/devices_test.py b/tests/unit_tests/cx_devices/devices_test.py
index 5abdca4f..87b6e8b1 100644
--- a/tests/unit_tests/cx_devices/devices_test.py
+++ b/tests/unit_tests/cx_devices/devices_test.py
@@ -60,6 +60,7 @@ def test_devices(device_class: Type[Controller]):
device.get_z2m_actions_mapping,
device.get_deconz_actions_mapping,
device.get_zha_actions_mapping,
+ device.get_lutron_caseta_actions_mapping,
]
for func in integration_mappings_funcs:
mappings = func()
diff --git a/tests/unit_tests/cx_devices/muller_licht_test.py b/tests/unit_tests/cx_devices/muller_licht_test.py
new file mode 100644
index 00000000..5de42583
--- /dev/null
+++ b/tests/unit_tests/cx_devices/muller_licht_test.py
@@ -0,0 +1,46 @@
+import pytest
+from cx_core.integration import EventData
+from cx_devices.muller_licht import MLI404002LightController
+
+
+@pytest.mark.parametrize(
+ "data, expected_action",
+ [
+ (
+ {"command": "on", "args": []},
+ "on",
+ ),
+ (
+ {"command": "off", "args": []},
+ "off",
+ ),
+ (
+ {"command": "step", "args": [0, 43, 3]},
+ "step_up",
+ ),
+ (
+ {"command": "move", "args": [0, 100]},
+ "move_up",
+ ),
+ (
+ {"command": "step", "args": [1, 43, 3]},
+ "step_down",
+ ),
+ (
+ {"command": "move", "args": [1, 100]},
+ "move_down",
+ ),
+ (
+ {"command": "stop", "args": []},
+ "stop",
+ ),
+ (
+ {"command": "recall", "args": [16387, 1]},
+ "recall",
+ ),
+ ],
+)
+def test_zha_action_MLI404002(data: EventData, expected_action: str):
+ sut = MLI404002LightController() # type: ignore
+ action = sut.get_zha_action(data)
+ assert action == expected_action