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