From a137bfc93879c5d9ac05bf6457e4185427bc958f Mon Sep 17 00:00:00 2001 From: Florian Krebs Date: Sat, 2 May 2026 14:31:07 +0200 Subject: [PATCH] chore: untrack vendor code, remove powerocean leftover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - voice_satellite/ — Wake-Word ML-Modell (extern trainiert, von TaterTotterson), nicht eigener Config; via .gitignore ausgenommen. - appdaemon/apps/controllerx/ — ControllerX v5.2.3 wird via HACS-AppDaemon gemanagt (xaviml/controllerx); via .gitignore ausgenommen. apps.yaml bleibt versioniert (eigene Mapping-Config). - powerocean_powerpulse/ — alte Version (2025.01.07) der niltrip/powerocean Komponente, lag im falschen Pfad (/config/ statt custom_components/), wurde nie geladen. Aktive Version ist custom_components/powerocean_dev/ (2026.05.01). Komplett entfernt. --- appdaemon/apps/controllerx/controllerx.py | 39 - appdaemon/apps/controllerx/cx_const.py | 150 --- .../apps/controllerx/cx_core/__init__.py | 18 - .../cx_core/action_type/__init__.py | 46 - .../controllerx/cx_core/action_type/base.py | 25 - .../action_type/call_service_action_type.py | 52 - .../cx_core/action_type/delay_action_type.py | 17 - .../action_type/predefined_action_type.py | 121 --- .../cx_core/action_type/scene_action_type.py | 17 - .../apps/controllerx/cx_core/color_helper.py | 81 -- .../apps/controllerx/cx_core/controller.py | 611 ----------- .../cx_core/feature_support/__init__.py | 40 - .../cx_core/feature_support/cover.py | 9 - .../cx_core/feature_support/light.py | 4 - .../cx_core/feature_support/media_player.py | 20 - .../cx_core/integration/__init__.py | 35 - .../controllerx/cx_core/integration/deconz.py | 34 - .../controllerx/cx_core/integration/event.py | 41 - .../cx_core/integration/homematic.py | 36 - .../cx_core/integration/lutron_caseta.py | 28 - .../controllerx/cx_core/integration/mqtt.py | 43 - .../controllerx/cx_core/integration/shelly.py | 25 - .../cx_core/integration/shellyforhass.py | 27 - .../controllerx/cx_core/integration/state.py | 28 - .../cx_core/integration/tasmota.py | 51 - .../controllerx/cx_core/integration/z2m.py | 85 -- .../controllerx/cx_core/integration/zha.py | 46 - .../cx_core/release_hold_controller.py | 69 -- .../controllerx/cx_core/stepper/__init__.py | 112 -- .../cx_core/stepper/bounce_stepper.py | 17 - .../cx_core/stepper/index_loop_stepper.py | 31 - .../cx_core/stepper/loop_stepper.py | 16 - .../cx_core/stepper/stop_stepper.py | 26 - .../apps/controllerx/cx_core/type/__init__.py | 0 .../cx_core/type/cover_controller.py | 125 --- .../cx_core/type/light_controller.py | 958 ------------------ .../cx_core/type/media_player_controller.py | 196 ---- .../cx_core/type/switch_controller.py | 48 - .../cx_core/type/z2m_light_controller.py | 429 -------- .../controllerx/cx_core/type_controller.py | 133 --- .../apps/controllerx/cx_devices/__init__.py | 0 appdaemon/apps/controllerx/cx_devices/adeo.py | 46 - .../apps/controllerx/cx_devices/aqara.py | 878 ---------------- .../apps/controllerx/cx_devices/aurora.py | 37 - .../apps/controllerx/cx_devices/homematic.py | 78 -- appdaemon/apps/controllerx/cx_devices/ikea.py | 844 --------------- .../apps/controllerx/cx_devices/legrand.py | 100 -- .../apps/controllerx/cx_devices/linkind.py | 38 - .../apps/controllerx/cx_devices/livarno.py | 47 - .../apps/controllerx/cx_devices/lutron.py | 167 --- .../controllerx/cx_devices/muller_licht.py | 118 --- .../apps/controllerx/cx_devices/namron.py | 54 - .../apps/controllerx/cx_devices/osram.py | 38 - .../apps/controllerx/cx_devices/philips.py | 326 ------ .../apps/controllerx/cx_devices/prolight.py | 34 - .../apps/controllerx/cx_devices/rgb_genie.py | 90 -- appdaemon/apps/controllerx/cx_devices/robb.py | 54 - .../apps/controllerx/cx_devices/sengled.py | 48 - .../apps/controllerx/cx_devices/shelly.py | 57 -- .../controllerx/cx_devices/smartkontakten.py | 25 - .../controllerx/cx_devices/smartthings.py | 58 -- .../apps/controllerx/cx_devices/sonoff.py | 23 - .../apps/controllerx/cx_devices/tasmota.py | 103 -- .../apps/controllerx/cx_devices/terncy.py | 52 - .../apps/controllerx/cx_devices/trust.py | 39 - appdaemon/apps/controllerx/cx_devices/tuya.py | 179 ---- appdaemon/apps/controllerx/cx_helper.py | 33 - appdaemon/apps/controllerx/cx_version.py | 1 - powerocean_powerpulse/__init__.py | 86 -- .../additional datapoints.json | 25 - powerocean_powerpulse/config_flow.py | 189 ---- powerocean_powerpulse/const.py | 38 - powerocean_powerpulse/datapoints.json | 72 -- powerocean_powerpulse/ecoflow.py | 501 --------- powerocean_powerpulse/icon.png | Bin 4830 -> 0 bytes powerocean_powerpulse/manifest.json | 12 - powerocean_powerpulse/samplereport.json | 635 ------------ powerocean_powerpulse/sensor.py | 375 ------- powerocean_powerpulse/strings.json | 31 - powerocean_powerpulse/translations/de.json | 31 - powerocean_powerpulse/translations/en.json | 31 - powerocean_powerpulse/variants/dcfit.json | 100 -- .../variants/powerocean.json | 45 - voice_satellite/models/hey_baby.json | 18 - voice_satellite/models/hey_baby.tflite | Bin 63520 -> 0 bytes 85 files changed, 9645 deletions(-) delete mode 100644 appdaemon/apps/controllerx/controllerx.py delete mode 100644 appdaemon/apps/controllerx/cx_const.py delete mode 100644 appdaemon/apps/controllerx/cx_core/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_core/action_type/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_core/action_type/base.py delete mode 100644 appdaemon/apps/controllerx/cx_core/action_type/call_service_action_type.py delete mode 100644 appdaemon/apps/controllerx/cx_core/action_type/delay_action_type.py delete mode 100644 appdaemon/apps/controllerx/cx_core/action_type/predefined_action_type.py delete mode 100644 appdaemon/apps/controllerx/cx_core/action_type/scene_action_type.py delete mode 100644 appdaemon/apps/controllerx/cx_core/color_helper.py delete mode 100644 appdaemon/apps/controllerx/cx_core/controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/feature_support/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_core/feature_support/cover.py delete mode 100644 appdaemon/apps/controllerx/cx_core/feature_support/light.py delete mode 100644 appdaemon/apps/controllerx/cx_core/feature_support/media_player.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/deconz.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/event.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/homematic.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/lutron_caseta.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/mqtt.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/shelly.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/shellyforhass.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/state.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/tasmota.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/z2m.py delete mode 100644 appdaemon/apps/controllerx/cx_core/integration/zha.py delete mode 100644 appdaemon/apps/controllerx/cx_core/release_hold_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/stepper/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_core/stepper/bounce_stepper.py delete mode 100644 appdaemon/apps/controllerx/cx_core/stepper/index_loop_stepper.py delete mode 100644 appdaemon/apps/controllerx/cx_core/stepper/loop_stepper.py delete mode 100644 appdaemon/apps/controllerx/cx_core/stepper/stop_stepper.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type/cover_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type/light_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type/media_player_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type/switch_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type/z2m_light_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_core/type_controller.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/__init__.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/adeo.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/aqara.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/aurora.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/homematic.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/ikea.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/legrand.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/linkind.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/livarno.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/lutron.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/muller_licht.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/namron.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/osram.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/philips.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/prolight.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/rgb_genie.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/robb.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/sengled.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/shelly.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/smartkontakten.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/smartthings.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/sonoff.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/tasmota.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/terncy.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/trust.py delete mode 100644 appdaemon/apps/controllerx/cx_devices/tuya.py delete mode 100644 appdaemon/apps/controllerx/cx_helper.py delete mode 100644 appdaemon/apps/controllerx/cx_version.py delete mode 100644 powerocean_powerpulse/__init__.py delete mode 100644 powerocean_powerpulse/additional datapoints.json delete mode 100644 powerocean_powerpulse/config_flow.py delete mode 100644 powerocean_powerpulse/const.py delete mode 100644 powerocean_powerpulse/datapoints.json delete mode 100644 powerocean_powerpulse/ecoflow.py delete mode 100644 powerocean_powerpulse/icon.png delete mode 100644 powerocean_powerpulse/manifest.json delete mode 100644 powerocean_powerpulse/samplereport.json delete mode 100644 powerocean_powerpulse/sensor.py delete mode 100644 powerocean_powerpulse/strings.json delete mode 100644 powerocean_powerpulse/translations/de.json delete mode 100644 powerocean_powerpulse/translations/en.json delete mode 100644 powerocean_powerpulse/variants/dcfit.json delete mode 100644 powerocean_powerpulse/variants/powerocean.json delete mode 100644 voice_satellite/models/hey_baby.json delete mode 100644 voice_satellite/models/hey_baby.tflite diff --git a/appdaemon/apps/controllerx/controllerx.py b/appdaemon/apps/controllerx/controllerx.py deleted file mode 100644 index 0f93898..0000000 --- a/appdaemon/apps/controllerx/controllerx.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Create controller-based automations with ease to control your home devices and scenes. - -https://github.com/xaviml/controllerx -""" - -from cx_core import ( - Controller, - CoverController, - LightController, - MediaPlayerController, - SwitchController, - Z2MLightController, -) -from cx_devices.adeo import * -from cx_devices.aqara import * -from cx_devices.aurora import * -from cx_devices.homematic import * -from cx_devices.ikea import * -from cx_devices.legrand import * -from cx_devices.linkind import * -from cx_devices.livarno import * -from cx_devices.lutron import * -from cx_devices.muller_licht import * -from cx_devices.namron import * -from cx_devices.osram import * -from cx_devices.philips import * -from cx_devices.prolight import * -from cx_devices.rgb_genie import * -from cx_devices.robb import * -from cx_devices.sengled import * -from cx_devices.shelly import * -from cx_devices.smartkontakten import * -from cx_devices.smartthings import * -from cx_devices.sonoff import * -from cx_devices.tasmota import * -from cx_devices.terncy import * -from cx_devices.trust import * -from cx_devices.tuya import * diff --git a/appdaemon/apps/controllerx/cx_const.py b/appdaemon/apps/controllerx/cx_const.py deleted file mode 100644 index 44265c8..0000000 --- a/appdaemon/apps/controllerx/cx_const.py +++ /dev/null @@ -1,150 +0,0 @@ -from collections.abc import Awaitable, Callable -from typing import Any, Optional, Union - -ActionFunction = Callable[..., Awaitable[Any]] -ActionParams = tuple[Any, ...] -ActionFunctionWithParams = tuple[ActionFunction, ActionParams] -TypeAction = Union[ActionFunction, ActionFunctionWithParams] -ActionEvent = Union[str, int] -PredefinedActionsMapping = dict[str, TypeAction] -DefaultActionsMapping = dict[ActionEvent, Optional[str]] - -CustomAction = Union[str, dict[str, Any]] -CustomActions = Union[list[CustomAction], CustomAction] -CustomActionsMapping = dict[ActionEvent, Optional[CustomActions]] - -Number = Union[int, float] - - -class Light: - ON = "on" - OFF = "off" - TOGGLE = "toggle" - TOGGLE_FULL_BRIGHTNESS = "toggle_full_brightness" - TOGGLE_FULL_WHITE_VALUE = "toggle_full_white_value" - TOGGLE_FULL_COLOR_TEMP = "toggle_full_color_temp" - TOGGLE_MIN_BRIGHTNESS = "toggle_min_brightness" - TOGGLE_MIN_WHITE_VALUE = "toggle_min_white_value" - TOGGLE_MIN_COLOR_TEMP = "toggle_min_color_temp" - RELEASE = "release" - ON_FULL_BRIGHTNESS = "on_full_brightness" - ON_FULL_WHITE_VALUE = "on_full_white_value" - ON_FULL_COLOR_TEMP = "on_full_color_temp" - ON_MIN_BRIGHTNESS = "on_min_brightness" - ON_MIN_WHITE_VALUE = "on_min_white_value" - ON_MIN_COLOR_TEMP = "on_min_color_temp" - ON_MIN_MAX_BRIGHTNESS = "on_min_max_brightness" - ON_MAX_MIN_BRIGHTNESS = "on_max_min_brightness" - ON_MIN_MAX_COLOR_TEMP = "on_min_max_color_temp" - ON_MAX_MIN_COLOR_TEMP = "on_max_min_color_temp" - SET_HALF_BRIGHTNESS = "set_half_brightness" - SET_HALF_WHITE_VALUE = "set_half_white_value" - SET_HALF_COLOR_TEMP = "set_half_color_temp" - SYNC = "sync" - CLICK = "click" - CLICK_BRIGHTNESS_UP = "click_brightness_up" - CLICK_BRIGHTNESS_DOWN = "click_brightness_down" - CLICK_WHITE_VALUE_UP = "click_white_value_up" - CLICK_WHITE_VALUE_DOWN = "click_white_value_down" - CLICK_COLOR_UP = "click_color_up" - CLICK_COLOR_DOWN = "click_color_down" - CLICK_COLOR_TEMP_UP = "click_colortemp_up" - CLICK_COLOR_TEMP_DOWN = "click_colortemp_down" - CLICK_XY_COLOR_UP = "click_xycolor_up" - CLICK_XY_COLOR_DOWN = "click_xycolor_down" - HOLD = "hold" - HOLD_BRIGHTNESS_UP = "hold_brightness_up" - HOLD_BRIGHTNESS_DOWN = "hold_brightness_down" - HOLD_BRIGHTNESS_TOGGLE = "hold_brightness_toggle" - HOLD_WHITE_VALUE_UP = "hold_white_value_up" - HOLD_WHITE_VALUE_DOWN = "hold_white_value_down" - HOLD_WHITE_VALUE_TOGGLE = "hold_white_value_toggle" - HOLD_COLOR_UP = "hold_color_up" - HOLD_COLOR_DOWN = "hold_color_down" - HOLD_COLOR_TOGGLE = "hold_color_toggle" - HOLD_COLOR_TEMP_UP = "hold_colortemp_up" - HOLD_COLOR_TEMP_DOWN = "hold_colortemp_down" - HOLD_COLOR_TEMP_TOGGLE = "hold_colortemp_toggle" - HOLD_XY_COLOR_UP = "hold_xycolor_up" - HOLD_XY_COLOR_DOWN = "hold_xycolor_down" - HOLD_XY_COLOR_TOGGLE = "hold_xycolor_toggle" - XYCOLOR_FROM_CONTROLLER = "xycolor_from_controller" - COLORTEMP_FROM_CONTROLLER = "colortemp_from_controller" - COLORTEMP_FROM_CONTROLLER_STEP = "colortemp_from_controller_step" - BRIGHTNESS_FROM_CONTROLLER_LEVEL = "brightness_from_controller_level" - BRIGHTNESS_FROM_CONTROLLER_ANGLE = "brightness_from_controller_angle" - BRIGHTNESS_FROM_CONTROLLER_STEP = "brightness_from_controller_step" - - -class Z2MLight: - ON = "on" - OFF = "off" - TOGGLE = "toggle" - RELEASE = "release" - ON_FULL_BRIGHTNESS = "on_full_brightness" - ON_FULL_COLOR_TEMP = "on_full_color_temp" - ON_MIN_BRIGHTNESS = "on_min_brightness" - ON_MIN_COLOR_TEMP = "on_min_color_temp" - SET_HALF_BRIGHTNESS = "set_half_brightness" - SET_HALF_COLOR_TEMP = "set_half_color_temp" - CLICK = "click" - CLICK_BRIGHTNESS_UP = "click_brightness_up" - CLICK_BRIGHTNESS_DOWN = "click_brightness_down" - CLICK_COLOR_TEMP_UP = "click_colortemp_up" - CLICK_COLOR_TEMP_DOWN = "click_colortemp_down" - HOLD = "hold" - HOLD_BRIGHTNESS_UP = "hold_brightness_up" - HOLD_BRIGHTNESS_DOWN = "hold_brightness_down" - HOLD_BRIGHTNESS_TOGGLE = "hold_brightness_toggle" - HOLD_COLOR_TEMP_UP = "hold_colortemp_up" - HOLD_COLOR_TEMP_DOWN = "hold_colortemp_down" - HOLD_COLOR_TEMP_TOGGLE = "hold_colortemp_toggle" - XYCOLOR_FROM_CONTROLLER = "xycolor_from_controller" - COLORTEMP_FROM_CONTROLLER = "colortemp_from_controller" - BRIGHTNESS_FROM_CONTROLLER_LEVEL = "brightness_from_controller_level" - BRIGHTNESS_FROM_CONTROLLER_ANGLE = "brightness_from_controller_angle" - - -class MediaPlayer: - HOLD_VOLUME_DOWN = "hold_volume_down" - HOLD_VOLUME_UP = "hold_volume_up" - CLICK_VOLUME_DOWN = "click_volume_down" - CLICK_VOLUME_UP = "click_volume_up" - VOLUME_SET = "volume_set" - RELEASE = "release" - PLAY = "play" - PAUSE = "pause" - PLAY_PAUSE = "play_pause" - NEXT_TRACK = "next_track" - PREVIOUS_TRACK = "previous_track" - NEXT_SOURCE = "next_source" - PREVIOUS_SOURCE = "previous_source" - MUTE = "mute" - TTS = "tts" - VOLUME_FROM_CONTROLLER_ANGLE = "volume_from_controller_angle" - - -class Switch: - ON = "on" - OFF = "off" - TOGGLE = "toggle" - - -class Cover: - OPEN = "open" - CLOSE = "close" - STOP = "stop" - TOGGLE_OPEN = "toggle_open" - TOGGLE_CLOSE = "toggle_close" - - -class StepperDir: - UP = "up" - DOWN = "down" - TOGGLE = "toggle" - - -class StepperMode: - STOP = "stop" - LOOP = "loop" - BOUNCE = "bounce" diff --git a/appdaemon/apps/controllerx/cx_core/__init__.py b/appdaemon/apps/controllerx/cx_core/__init__.py deleted file mode 100644 index 539ec28..0000000 --- a/appdaemon/apps/controllerx/cx_core/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from cx_core.controller import Controller, action -from cx_core.release_hold_controller import ReleaseHoldController -from cx_core.type.cover_controller import CoverController -from cx_core.type.light_controller import LightController -from cx_core.type.media_player_controller import MediaPlayerController -from cx_core.type.switch_controller import SwitchController -from cx_core.type.z2m_light_controller import Z2MLightController - -__all__ = [ - "Controller", - "ReleaseHoldController", - "LightController", - "Z2MLightController", - "MediaPlayerController", - "SwitchController", - "CoverController", - "action", -] diff --git a/appdaemon/apps/controllerx/cx_core/action_type/__init__.py b/appdaemon/apps/controllerx/cx_core/action_type/__init__.py deleted file mode 100644 index 37f8e37..0000000 --- a/appdaemon/apps/controllerx/cx_core/action_type/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import TYPE_CHECKING - -from cx_const import ActionEvent, CustomAction, CustomActions -from cx_core.action_type.base import ActionType -from cx_core.action_type.call_service_action_type import CallServiceActionType -from cx_core.action_type.delay_action_type import DelayActionType -from cx_core.action_type.predefined_action_type import PredefinedActionType -from cx_core.action_type.scene_action_type import SceneActionType - -if TYPE_CHECKING: - from cx_core import Controller - -ActionsMapping = dict[ActionEvent, list[ActionType]] - -action_type_mapping: dict[str, type[ActionType]] = { - "action": PredefinedActionType, - "service": CallServiceActionType, - "scene": SceneActionType, - "delay": DelayActionType, -} - - -def parse_actions(controller: "Controller", data: CustomActions) -> list[ActionType]: - actions: CustomActions - if isinstance(data, (list, tuple)): - actions = list(data) - else: - actions = [data] - - return [_parse_action(controller, action) for action in actions] - - -def _parse_action(controller: "Controller", action: CustomAction) -> ActionType: - if isinstance(action, str): - return PredefinedActionType(controller, {"action": action}) - try: - return next( - action_type(controller, action) - for key in action - for action_type_key, action_type in action_type_mapping.items() - if key == action_type_key - ) - except StopIteration: - raise ValueError( - f"Not able to parse `{action}`. Available keys are: {list(action_type_mapping.keys())}" - ) diff --git a/appdaemon/apps/controllerx/cx_core/action_type/base.py b/appdaemon/apps/controllerx/cx_core/action_type/base.py deleted file mode 100644 index 9aa361e..0000000 --- a/appdaemon/apps/controllerx/cx_core/action_type/base.py +++ /dev/null @@ -1,25 +0,0 @@ -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any - -from cx_core.integration import EventData - -if TYPE_CHECKING: - from cx_core import Controller - - -class ActionType(ABC): - controller: "Controller" - - def __init__(self, controller: "Controller", action: dict[str, Any]) -> None: - self.controller = controller - self.initialize(**action) - - def initialize(self, **kwargs: Any) -> None: - pass - - @abstractmethod - async def run(self, extra: EventData | None = None) -> None: - raise NotImplementedError - - def __str__(self) -> str: - return f"{self.__class__.__name__}" diff --git a/appdaemon/apps/controllerx/cx_core/action_type/call_service_action_type.py b/appdaemon/apps/controllerx/cx_core/action_type/call_service_action_type.py deleted file mode 100644 index 3f6fae0..0000000 --- a/appdaemon/apps/controllerx/cx_core/action_type/call_service_action_type.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import TYPE_CHECKING, Any, cast - -from cx_core.action_type.base import ActionType -from cx_core.integration import EventData - -if TYPE_CHECKING: - from cx_core.type_controller import Entity, TypeController - - -class CallServiceActionType(ActionType): - service: str - # Priority order for entity_id: - # - Inside data - # - In the same level as "service" - # - From the main config if the domain matches - entity_id: str | None - data: dict[str, Any] - - def initialize(self, **kwargs: Any) -> None: - self.service = kwargs["service"] - self.data = kwargs.get("data", {}) - - self.entity_id = self.data.get("entity_id") or kwargs.get("entity_id") - if ( - self.entity_id is None - and self._check_controller_isinstance_type_controller() - ): - type_controller = cast("TypeController[Entity]", self.controller) - if self._get_service_domain(self.service) in type_controller.domains: - self.entity_id = type_controller.entity.name - if "entity_id" in self.data: - del self.data["entity_id"] - - def _check_controller_isinstance_type_controller(self) -> bool: - # This is checked dynamically without the isinstance to avoid - # circular dependency - class_names = [c.__name__ for c in type(self.controller).mro()] - return "TypeController" in class_names - - def _get_service_domain(self, service: str) -> str: - return service.replace(".", "/").split("/")[0] - - async def run(self, extra: EventData | None = None) -> None: - if self.entity_id: - await self.controller.call_service( - self.service, entity_id=self.entity_id, **self.data - ) - else: - await self.controller.call_service(self.service, **self.data) - - def __str__(self) -> str: - return f"Service ({self.service})" diff --git a/appdaemon/apps/controllerx/cx_core/action_type/delay_action_type.py b/appdaemon/apps/controllerx/cx_core/action_type/delay_action_type.py deleted file mode 100644 index 6978dca..0000000 --- a/appdaemon/apps/controllerx/cx_core/action_type/delay_action_type.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from cx_core.action_type.base import ActionType -from cx_core.integration import EventData - - -class DelayActionType(ActionType): - delay: int - - def initialize(self, **kwargs: Any) -> None: - self.delay = kwargs["delay"] - - async def run(self, extra: EventData | None = None) -> None: - await self.controller.sleep(self.delay) - - def __str__(self) -> str: - return f"Delay ({self.delay} seconds)" diff --git a/appdaemon/apps/controllerx/cx_core/action_type/predefined_action_type.py b/appdaemon/apps/controllerx/cx_core/action_type/predefined_action_type.py deleted file mode 100644 index c162409..0000000 --- a/appdaemon/apps/controllerx/cx_core/action_type/predefined_action_type.py +++ /dev/null @@ -1,121 +0,0 @@ -import inspect -from typing import Any - -from cx_const import ( - ActionFunction, - ActionFunctionWithParams, - ActionParams, - PredefinedActionsMapping, - TypeAction, -) -from cx_core.action_type.base import ActionType -from cx_core.integration import EventData - - -def _get_action(action_value: TypeAction) -> ActionFunctionWithParams: - if isinstance(action_value, tuple): - return action_value - else: - return (action_value, tuple()) - - -def _get_arguments( - action: ActionFunction, - args: ActionParams, - predefined_action_kwargs: dict[str, Any], - extra: EventData | None, -) -> tuple[ActionParams, dict[str, Any]]: - action_parameters = inspect.signature(action).parameters - action_parameters_without_extra = { - key: param for key, param in action_parameters.items() if key != "extra" - } - action_parameters_without_default = { - key: param - for key, param in action_parameters.items() - if param.default is inspect.Signature.empty - } - action_args: dict[str, Any] = dict( - zip(action_parameters_without_extra.keys(), args) - ) # ControllerX args - action_positional_args = set(action_args.keys()) - action_args.update(predefined_action_kwargs) # User args - action_args.update({"extra": extra} if "extra" in action_parameters else {}) - action_args = { - key: value for key, value in action_args.items() if key in action_parameters - } - - if len(set(action_parameters_without_default).difference(action_args)) != 0: - error_msg = [ - f"`{action.__name__}` action is missing some parameters. Parameters available:" - ] - for key, param in action_parameters_without_extra.items(): - if hasattr(param.annotation, "__name__"): - attr_msg = f" {key}: {param.annotation.__name__}" - else: - attr_msg = f" {key}:" - if param.default is not inspect.Signature.empty: - attr_msg += f" [default: {param.default}]" - if key in action_args: - attr_msg += f" (value given: {action_args[key]})" - elif param.default is inspect.Signature.empty: - attr_msg += " (missing)" - error_msg.append(attr_msg) - raise ValueError("\n".join(error_msg)) - - positional = tuple( - value for key, value in action_args.items() if key in action_positional_args - ) - action_args = { - key: value - for key, value in action_args.items() - if key not in action_positional_args - } - return positional, action_args - - -class PredefinedActionType(ActionType): - predefined_action_key: str - predefined_action_kwargs: dict[str, Any] - predefined_actions_mapping: PredefinedActionsMapping - - def _raise_action_key_not_found( - self, predefined_action_key: str, predefined_actions: PredefinedActionsMapping - ) -> None: - raise ValueError( - f"`{predefined_action_key}` is not one of the predefined actions. " - f"Available actions are: {list(predefined_actions.keys())}." - "See more in: https://xaviml.github.io/controllerx/advanced/custom-controllers" - ) - - def initialize(self, **kwargs: Any) -> None: - self.predefined_action_key = kwargs.pop("action") - self.predefined_action_kwargs = kwargs - self.predefined_actions_mapping = ( - self.controller.get_predefined_actions_mapping() - ) - if not self.predefined_actions_mapping: - raise ValueError( - f"Cannot use predefined actions for `{self.controller.__class__.__name__}` class." - ) - if ( - not self.controller.contains_templating(self.predefined_action_key) - and self.predefined_action_key not in self.predefined_actions_mapping - ): - self._raise_action_key_not_found( - self.predefined_action_key, self.predefined_actions_mapping - ) - - async def run(self, extra: EventData | None = None) -> None: - action_key = await self.controller.render_value(self.predefined_action_key) - if action_key not in self.predefined_actions_mapping: - self._raise_action_key_not_found( - action_key, self.predefined_actions_mapping - ) - action, args = _get_action(self.predefined_actions_mapping[action_key]) - positional, action_args = _get_arguments( - action, args, self.predefined_action_kwargs, extra - ) - await action(*positional, **action_args) - - def __str__(self) -> str: - return f"Predefined ({self.predefined_action_key})" diff --git a/appdaemon/apps/controllerx/cx_core/action_type/scene_action_type.py b/appdaemon/apps/controllerx/cx_core/action_type/scene_action_type.py deleted file mode 100644 index 76af527..0000000 --- a/appdaemon/apps/controllerx/cx_core/action_type/scene_action_type.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from cx_core.action_type.base import ActionType -from cx_core.integration import EventData - - -class SceneActionType(ActionType): - scene: str - - def initialize(self, **kwargs: Any) -> None: - self.scene = kwargs["scene"] - - async def run(self, extra: EventData | None = None) -> None: - await self.controller.call_service("scene/turn_on", entity_id=self.scene) - - def __str__(self) -> str: - return f"Scene ({self.scene})" diff --git a/appdaemon/apps/controllerx/cx_core/color_helper.py b/appdaemon/apps/controllerx/cx_core/color_helper.py deleted file mode 100644 index ab4f646..0000000 --- a/appdaemon/apps/controllerx/cx_core/color_helper.py +++ /dev/null @@ -1,81 +0,0 @@ -Color = tuple[float, float] -Colors = list[Color] - -# These are the 24 colors that appear in the circle color of home assistant -default_color_wheel = [ - (0.701, 0.299), - (0.667, 0.284), - (0.581, 0.245), - (0.477, 0.196), - (0.385, 0.155), - (0.301, 0.116), - (0.217, 0.077), - (0.157, 0.05), - (0.136, 0.04), - (0.137, 0.065), - (0.141, 0.137), - (0.146, 0.238), - (0.323, 0.329), # 12; white color middle - (0.151, 0.343), - (0.157, 0.457), - (0.164, 0.591), - (0.17, 0.703), - (0.172, 0.747), - (0.199, 0.724), - (0.269, 0.665), - (0.36, 0.588), - (0.444, 0.517), - (0.527, 0.447), - (0.612, 0.374), - (0.677, 0.319), -] - -# These are the xy colors translated from color temperature (2000K to 6488K) -# They were extracted from here https://www.waveformlighting.com/files/blackBodyLocus_1.txt -color_temp_wheel = [ - (0.527, 0.413), - (0.507, 0.415), - (0.489, 0.415), - (0.472, 0.413), - (0.456, 0.41), - (0.442, 0.406), - (0.428, 0.401), - (0.416, 0.396), - (0.406, 0.391), - (0.396, 0.386), - (0.386, 0.38), - (0.378, 0.375), - (0.37, 0.37), - (0.363, 0.365), - (0.357, 0.361), - (0.351, 0.358), - (0.345, 0.355), - (0.34, 0.352), - (0.336, 0.349), - (0.331, 0.346), - (0.327, 0.343), - (0.323, 0.339), - (0.32, 0.336), - (0.317, 0.333), - (0.314, 0.33), -] - -COLOR_WHEELS = { - "default_color_wheel": default_color_wheel, - "color_temp_wheel": color_temp_wheel, -} - - -def get_color_wheel(colors: str | Colors) -> Colors: - if isinstance(colors, str): - if colors not in COLOR_WHEELS: - raise ValueError( - f"`{colors}` is not an option for `color_wheel`. Options are: {list(COLOR_WHEELS.keys())}" - ) - return COLOR_WHEELS[colors] - elif isinstance(colors, (list, tuple)): - return colors - else: - raise ValueError( - f"Type {type(colors)} is not supported for `color_wheel` attribute" - ) diff --git a/appdaemon/apps/controllerx/cx_core/controller.py b/appdaemon/apps/controllerx/cx_core/controller.py deleted file mode 100644 index 5659c16..0000000 --- a/appdaemon/apps/controllerx/cx_core/controller.py +++ /dev/null @@ -1,611 +0,0 @@ -import asyncio -import re -import time -from ast import literal_eval -from asyncio import CancelledError, Task -from collections import Counter, defaultdict -from collections.abc import Awaitable, Callable -from functools import wraps -from typing import ( - Any, - DefaultDict, - Literal, - Optional, - TypeVar, - overload, -) - -import appdaemon.utils as utils -import cx_core.integration as integration_module -import cx_version -from appdaemon.adapi import ADAPI -from appdaemon.plugins.hass.hassapi import Hass -from appdaemon.plugins.mqtt.mqttapi import Mqtt -from cx_const import ( - ActionEvent, - ActionFunction, - CustomActionsMapping, - DefaultActionsMapping, - PredefinedActionsMapping, -) -from cx_core.action_type import ActionsMapping, parse_actions -from cx_core.action_type.base import ActionType -from cx_core.integration import EventData, Integration - -DEFAULT_ACTION_DELTA = 300 # In milliseconds -DEFAULT_MULTIPLE_CLICK_DELAY = 500 # In milliseconds -MULTIPLE_CLICK_TOKEN = "$" - -MODE_SINGLE = "single" -MODE_RESTART = "restart" -MODE_QUEUED = "queued" -MODE_PARALLEL = "parallel" - -T = TypeVar("T") - - -def action(method: Callable[..., Awaitable[Any]]) -> ActionFunction: - @wraps(method) - async def _action_impl(controller: "Controller", *args: Any, **kwargs: Any) -> None: - continue_call = await controller.before_action(method.__name__, *args, **kwargs) - if continue_call: - await method(controller, *args, **kwargs) - - return _action_impl - - -def run_in(fn: Callable[..., Any], delay: float, **kwargs: Any) -> "Task[None]": - """ - It runs the function (fn) in running event loop in `delay` seconds. - This function has been created because the default run_in function - from AppDaemon does not accept microseconds. - """ - - async def inner() -> None: - await asyncio.sleep(delay) - await fn(kwargs) - - task = asyncio.create_task(inner()) - return task - - -class Controller(Hass, Mqtt): # type: ignore[misc] - """ - This is the parent Controller, all controllers must extend from this class. - """ - - args: dict[str, Any] - integration: Integration - actions_mapping: ActionsMapping - action_handles: DefaultDict[ActionEvent, Optional["Task[None]"]] - action_delay_handles: dict[ActionEvent, str | None] - multiple_click_actions: set[ActionEvent] - action_delay: dict[ActionEvent, int] - action_delta: dict[ActionEvent, int] - action_times: dict[str, float] - previous_states: dict[ActionEvent, str | None] - multiple_click_action_times: dict[str, float] - click_counter: Counter[ActionEvent] - multiple_click_action_delay_tasks: DefaultDict[ActionEvent, Optional["Task[None]"]] - multiple_click_delay: int - - async def initialize(self) -> None: - self.log(f"🎮 ControllerX {cx_version.__version__}", ascii_encode=False) - await self.init() - - async def init(self) -> None: - controllers_ids: list[str] = self.get_list(self.args["controller"]) - self.integration = self.get_integration(self.args["integration"]) - - if "mapping" in self.args and "merge_mapping" in self.args: - raise ValueError("`mapping` and `merge_mapping` cannot be used together") - - custom_mapping: CustomActionsMapping | None = self.args.get("mapping", None) - merge_mapping: CustomActionsMapping | None = self.args.get( - "merge_mapping", None - ) - - if custom_mapping is None: - default_actions_mapping = self.get_default_actions_mapping(self.integration) - self.actions_mapping = self.parse_action_mapping(default_actions_mapping) # type: ignore[arg-type] - else: - self.actions_mapping = self.parse_action_mapping(custom_mapping) - - if merge_mapping is not None: - self.actions_mapping.update(self.parse_action_mapping(merge_mapping)) - - # Filter actions with include and exclude - if "actions" in self.args and "excluded_actions" in self.args: - raise ValueError("`actions` and `excluded_actions` cannot be used together") - include: list[ActionEvent] = self.get_list( - self.args.get("actions", list(self.actions_mapping.keys())) - ) - exclude: list[ActionEvent] = self.get_list( - self.args.get("excluded_actions", []) - ) - self.actions_mapping = self.filter_actions( - self.actions_mapping, set(include), set(exclude) - ) - - # Action delay - self.action_delay = self.get_mapping_per_action( - self.actions_mapping, - custom=self.args.get("action_delay"), - default=0, - ) - self.action_delay_handles = defaultdict(lambda: None) - self.action_handles = defaultdict(lambda: None) - - # Action delta - self.action_delta = self.get_mapping_per_action( - self.actions_mapping, - custom=self.args.get("action_delta"), - default=DEFAULT_ACTION_DELTA, - ) - self.action_times = defaultdict(float) - - # Previous state - self.previous_states = self.get_mapping_per_action( - self.actions_mapping, - custom=self.args.get("previous_state"), - default=None, - ) - - # Multiple click - self.multiple_click_actions = self.get_multiple_click_actions( - self.actions_mapping - ) - self.multiple_click_delay = self.args.get( - "multiple_click_delay", DEFAULT_MULTIPLE_CLICK_DELAY - ) - self.multiple_click_action_times = defaultdict(float) - self.click_counter = Counter() - self.multiple_click_action_delay_tasks = defaultdict(lambda: None) - - # Mode - self.mode = self.get_mapping_per_action( - self.actions_mapping, custom=self.args.get("mode"), default=MODE_SINGLE - ) - - # Listen for device changes - for controller_id in controllers_ids: - await self.integration.listen_changes(controller_id) - - def filter_actions( - self, - actions_mapping: ActionsMapping, - include: set[ActionEvent], - exclude: set[ActionEvent], - ) -> ActionsMapping: - allowed_actions = include - exclude - return { - key: value - for key, value in actions_mapping.items() - if key in allowed_actions - } - - @staticmethod - def get_option(value: str, options: list[str], ctx: str | None = None) -> str: - if value in options: - return value - else: - raise ValueError( - f"{f'{ctx} - ' if ctx is not None else ''}`{value}` is not an option. " - f"The options are {options}" - ) - - def parse_integration( - self, integration: str | dict[str, Any] | Any - ) -> dict[str, str]: - if isinstance(integration, str): - return {"name": integration} - elif isinstance(integration, dict): - if "name" in integration: - return integration - else: - raise ValueError("'name' attribute is mandatory") - else: - raise ValueError( - f"Type {type(integration)} is not supported for `integration` attribute" - ) - - def get_integration(self, integration: str | dict[str, Any]) -> Integration: - parsed_integration = self.parse_integration(integration) - kwargs = {k: v for k, v in parsed_integration.items() if k != "name"} - integrations = integration_module.get_integrations(self, kwargs) - integration_argument = self.get_option( - parsed_integration["name"], [i.name for i in integrations] - ) - return next(i for i in integrations if i.name == integration_argument) - - def get_default_actions_mapping( - self, integration: Integration - ) -> DefaultActionsMapping: - actions_mapping = integration.get_default_actions_mapping() - if actions_mapping is None: - raise ValueError( - f"This controller does not support {integration.name}. Use `mapping` to define the actions." - ) - return actions_mapping - - @overload - def get_list(self, entities: list[T]) -> list[T]: - pass - - @overload - def get_list(self, entities: T) -> list[T]: - pass - - def get_list(self, entities: list[T] | T) -> list[T]: - if isinstance(entities, (list, tuple)): - return list(entities) - return [entities] - - @overload - def get_mapping_per_action( - self, - actions_mapping: ActionsMapping, - *, - custom: T | dict[ActionEvent, T] | None, - default: None, - ) -> dict[ActionEvent, T | None]: - pass - - @overload - def get_mapping_per_action( - self, - actions_mapping: ActionsMapping, - *, - custom: T | dict[ActionEvent, T] | None, - default: T, - ) -> dict[ActionEvent, T]: - pass - - def get_mapping_per_action( - self, - actions_mapping: ActionsMapping, - *, - custom: T | dict[ActionEvent, T] | None, - default: None | T, - ) -> dict[ActionEvent, T | None] | dict[ActionEvent, T]: - if custom is not None and not isinstance(custom, dict): - default = custom - mapping = {action: default for action in actions_mapping} - if custom is not None and isinstance(custom, dict): - mapping.update(custom) - return mapping - - def parse_action_mapping(self, mapping: CustomActionsMapping) -> ActionsMapping: - return { - event: parse_actions(self, action) - for event, action in mapping.items() - if action is not None - } - - def get_multiple_click_actions(self, mapping: ActionsMapping) -> set[ActionEvent]: - to_return: set[ActionEvent] = set() - for key in mapping.keys(): - if not isinstance(key, str) or MULTIPLE_CLICK_TOKEN not in key: - continue - splitted = key.split(MULTIPLE_CLICK_TOKEN) - assert 1 <= len(splitted) <= 2 - action_key, _ = splitted - try: - to_return.add(int(action_key)) - except ValueError: - to_return.add(action_key) - return to_return - - def format_multiple_click_action( - self, action_key: ActionEvent, click_count: int - ) -> str: - return ( - str(action_key) + MULTIPLE_CLICK_TOKEN + str(click_count) - ) # e.g. toggle$2 - - async def _render_template(self, template: str) -> Any: - result = await self.render_template(template) - if result is None: - raise ValueError(f"Template {template} returned None") - try: - return literal_eval(result) - except (SyntaxError, ValueError): - return result - - _TEMPLATE_RE = re.compile(r"\s*\{\{.*\}\}") - - def contains_templating(self, template: str) -> bool: - is_template = self._TEMPLATE_RE.search(template) is not None - if not is_template: - self.log(f"`{template}` is not recognized as a template", level="DEBUG") - return is_template - - async def render_value(self, value: Any) -> Any: - if isinstance(value, str) and self.contains_templating(value): - return await self._render_template(value) - else: - return value - - async def render_attributes(self, attributes: dict[str, Any]) -> dict[str, Any]: - new_attributes: dict[str, Any] = {} - for key, value in attributes.items(): - new_value = await self.render_value(value) - if isinstance(value, dict): - new_value = await self.render_attributes(value) - new_attributes[key] = new_value - return new_attributes - - async def call_service( - self, service: str, render_template: bool = True, **attributes: Any - ) -> Any | None: - service = service.replace(".", "/") - to_log = ["\n", f"🤖 Service: \033[1m{service.replace('/', '.')}\033[0m"] - if service != "template/render" and render_template: - attributes = await self.render_attributes(attributes) - for attribute, value in attributes.items(): - if isinstance(value, float): - value = f"{value:.2f}" - to_log.append(f" - {attribute}: {value}") - self.log("\n".join(to_log), level="INFO", ascii_encode=False) - return await ADAPI.call_service(self, service, **attributes) - - @utils.sync_decorator # type: ignore[untyped-decorator] - async def get_state( - self, - entity_id: str | None = None, - attribute: str | Literal["all"] | None = None, - default: Any | None = None, - namespace: str | None = None, - copy: bool = True, - **kwargs: dict[str, Any], # left in intentionally for compatibility - ) -> Any | dict[str, Any] | None: - rendered_entity_id = await self.render_value(entity_id) - return await super().get_state( - rendered_entity_id, attribute, default=default, copy=copy - ) - - async def handle_action( - self, - action_key: str, - previous_state: str | None = None, - extra: EventData | None = None, - ) -> None: - if ( - action_key in self.actions_mapping - and self.previous_states[action_key] is not None - and previous_state != self.previous_states[action_key] - ): - self.log( - f"🎮 `{action_key}` not triggered because previous action was `{previous_state}`", - level="DEBUG", - ascii_encode=False, - ) - return - if ( - action_key in self.actions_mapping - and action_key not in self.multiple_click_actions - ): - previous_call_time = self.action_times[action_key] - now = time.time() * 1000 - self.action_times[action_key] = now - if now - previous_call_time > self.action_delta[action_key]: - await self.call_action(action_key, extra=extra) - elif action_key in self.multiple_click_actions: - now = time.time() * 1000 - previous_call_time = self.multiple_click_action_times.get(action_key, now) - self.multiple_click_action_times[action_key] = now - if now - previous_call_time > self.multiple_click_delay: - pass - - previous_task = self.multiple_click_action_delay_tasks[action_key] - if previous_task is not None: - previous_task.cancel() - - self.click_counter[action_key] += 1 - click_count = self.click_counter[action_key] - - new_task = run_in( - self.multiple_click_call_action, - self.multiple_click_delay / 1000, - action_key=action_key, - extra=extra, - click_count=click_count, - ) - self.multiple_click_action_delay_tasks[action_key] = new_task - else: - self.log( - f"🎮 Button event triggered, but not registered: `{action_key}`", - level="DEBUG", - ascii_encode=False, - ) - - async def multiple_click_call_action(self, kwargs: dict[str, Any]) -> None: - action_key: ActionEvent = kwargs["action_key"] - extra: EventData = kwargs["extra"] - click_count: int = kwargs["click_count"] - self.multiple_click_action_delay_tasks[action_key] = None - self.log( - f"🎮 {action_key} clicked `{click_count}` time(s)", - level="DEBUG", - ascii_encode=False, - ) - self.click_counter[action_key] = 0 - click_action_key = self.format_multiple_click_action(action_key, click_count) - if click_action_key in self.actions_mapping: - await self.call_action(click_action_key, extra=extra) - elif action_key in self.actions_mapping and click_count == 1: - await self.call_action(action_key, extra=extra) - - async def call_action( - self, action_key: ActionEvent, extra: EventData | None = None - ) -> None: - self.log( - f"🎮 Button event triggered: `{action_key}`", - level="INFO", - ascii_encode=False, - ) - self.log( - f"Extra:\n{extra}", - level="DEBUG", - ) - delay = self.action_delay[action_key] - if delay > 0: - handle = self.action_delay_handles[action_key] - if handle is not None: - await self.cancel_timer(handle) - self.log( - f"🕒 Running action(s) from `{action_key}` in {delay} seconds", - level="INFO", - ascii_encode=False, - ) - new_handle = await self.run_in( - self.action_timer_callback, delay, action_key=action_key, extra=extra - ) - self.action_delay_handles[action_key] = new_handle - else: - await self.action_timer_callback({"action_key": action_key, "extra": extra}) - - async def _apply_mode_strategy(self, action_key: ActionEvent) -> bool: - previous_task = self.action_handles[action_key] - if previous_task is None or previous_task.done(): - return False - if self.mode[action_key] == MODE_SINGLE: - self.log( - f"There is already an action executing for `{action_key}`. " - "If you want a different behaviour change `mode` parameter, " - "the default value is `single`.", - level="WARNING", - ) - return True - elif self.mode[action_key] == MODE_RESTART: - previous_task.cancel() - elif self.mode[action_key] == MODE_QUEUED: - await previous_task - elif self.mode[action_key] == MODE_PARALLEL: - pass - else: - raise ValueError( - f"`{self.mode[action_key]}` is not a possible value for `mode` parameter." - "Possible values: `single`, `restart`, `queued` and `parallel`." - ) - return False - - async def action_timer_callback(self, kwargs: dict[str, Any]) -> None: - action_key: ActionEvent = kwargs["action_key"] - extra: EventData = kwargs["extra"] - self.action_delay_handles[action_key] = None - skip = await self._apply_mode_strategy(action_key) - if skip: - return - action_types = self.actions_mapping[action_key] - task = asyncio.create_task(self.call_action_types(action_types, extra)) - self.action_handles[action_key] = task - try: - await task - except CancelledError: - self.log( - f"Task(s) from `{action_key}` was/were canceled and executed again", - level="DEBUG", - ) - - async def call_action_types( - self, action_types: list[ActionType], extra: EventData | None = None - ) -> None: - for action_type in action_types: - self.log( - f"🏃 Running `{action_type}` now", - level="INFO", - ascii_encode=False, - ) - await action_type.run(extra=extra) - - async def before_action(self, action: str, *args: str, **kwargs: Any) -> bool: - """ - Controllers have the option to implement this function, which is called - everytime before an action is called and it has the check_before_action decorator. - It should return True if the action shoul be called. - Otherwise it should return False. - """ - return True - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping | None: - """ - Controllers can implement this function. It should return a dict - with the states that a controller can take and the functions as values. - This is used for zigbee2mqtt support. - """ - return None - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping | None: - """ - Controllers can implement this function. It should return a dict - with the event id that a controller can take and the functions as values. - This is used for deCONZ support. - """ - return None - - def get_zha_actions_mapping(self) -> DefaultActionsMapping | None: - """ - 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 ZHA support. - """ - return None - - def get_zha_action(self, data: EventData) -> str | None: - """ - This method can be override for controllers that do not support - the standard extraction of the actions on cx_core/integration/zha.py - """ - return None - - def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping | None: - """ - 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_state_actions_mapping(self) -> DefaultActionsMapping | None: - """ - 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 State integration support. - """ - return None - - def get_homematic_actions_mapping(self) -> DefaultActionsMapping | None: - """ - 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 Homematic support. - """ - return None - - def get_shelly_actions_mapping(self) -> DefaultActionsMapping | None: - """ - 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) -> DefaultActionsMapping | None: - """ - 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_tasmota_actions_mapping(self) -> DefaultActionsMapping | None: - """ - 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 Tasmota support. - """ - return None - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - return {} diff --git a/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py b/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py deleted file mode 100644 index 5e22c61..0000000 --- a/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from cx_core.type_controller import Entity, TypeController - - -class FeatureSupport: - controller: "TypeController[Entity]" - update_supported_features: bool - _supported_features: int | None - - def __init__( - self, - controller: "TypeController[Entity]", - supported_features: int | None = None, - update_supported_features: bool = False, - ) -> None: - self.controller = controller - self._supported_features = supported_features - self.update_supported_features = update_supported_features - - @property - async def supported_features(self) -> int: - if self._supported_features is None or self.update_supported_features: - bitfield: str = await self.controller.get_entity_state( - attribute="supported_features" - ) - if bitfield is not None: - self._supported_features = int(bitfield) - else: - raise ValueError( - f"`supported_features` could not be read from `{self.controller.entity}`. Entity might not be available." - ) - return self._supported_features - - async def is_supported(self, feature: int) -> bool: - return feature & await self.supported_features != 0 - - async def not_supported(self, feature: int) -> bool: - return not await self.is_supported(feature) diff --git a/appdaemon/apps/controllerx/cx_core/feature_support/cover.py b/appdaemon/apps/controllerx/cx_core/feature_support/cover.py deleted file mode 100644 index 5fd8ee2..0000000 --- a/appdaemon/apps/controllerx/cx_core/feature_support/cover.py +++ /dev/null @@ -1,9 +0,0 @@ -class CoverSupport: - OPEN = 1 - CLOSE = 2 - SET_COVER_POSITION = 4 - STOP = 8 - OPEN_TILT = 16 - CLOSE_TILT = 32 - STOP_TILT = 64 - SET_TILT_POSITION = 128 diff --git a/appdaemon/apps/controllerx/cx_core/feature_support/light.py b/appdaemon/apps/controllerx/cx_core/feature_support/light.py deleted file mode 100644 index d7b501b..0000000 --- a/appdaemon/apps/controllerx/cx_core/feature_support/light.py +++ /dev/null @@ -1,4 +0,0 @@ -class LightSupport: - EFFECT = 4 - FLASH = 8 - TRANSITION = 32 diff --git a/appdaemon/apps/controllerx/cx_core/feature_support/media_player.py b/appdaemon/apps/controllerx/cx_core/feature_support/media_player.py deleted file mode 100644 index 319aaab..0000000 --- a/appdaemon/apps/controllerx/cx_core/feature_support/media_player.py +++ /dev/null @@ -1,20 +0,0 @@ -class MediaPlayerSupport: - PAUSE = 1 - SEEK = 2 - VOLUME_SET = 4 - VOLUME_MUTE = 8 - PREVIOUS_TRACK = 16 - NEXT_TRACK = 32 - TURN_ON = 128 - TURN_OFF = 256 - PLAY_MEDIA = 512 - VOLUME_STEP = 1024 - SELECT_SOURCE = 2048 - STOP = 4096 - CLEAR_PLAYLIST = 8192 - PLAY = 16384 - SHUFFLE_SET = 32768 - SELECT_SOUND_MODE = 65536 - SUPPORT_BROWSE_MEDIA = 131072 - SUPPORT_REPEAT_SET = 262144 - SUPPORT_GROUPING = 524288 diff --git a/appdaemon/apps/controllerx/cx_core/integration/__init__.py b/appdaemon/apps/controllerx/cx_core/integration/__init__.py deleted file mode 100644 index 4da0b05..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -import abc -from typing import TYPE_CHECKING, Any - -from cx_const import DefaultActionsMapping -from cx_helper import get_classes - -if TYPE_CHECKING: - from cx_core.controller import Controller - -EventData = dict[str, Any] - - -class Integration(abc.ABC): - name: str - controller: "Controller" - kwargs: dict[str, Any] - - def __init__(self, controller: "Controller", kwargs: dict[str, Any]): - self.controller = controller - self.kwargs = kwargs - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return None - - @abc.abstractmethod - async def listen_changes(self, controller_id: str) -> None: - raise NotImplementedError - - -def get_integrations( - controller: "Controller", kwargs: dict[str, Any] -) -> list[Integration]: - integration_classes = get_classes(__file__, __package__, Integration) - integrations = [cls_(controller, kwargs) for cls_ in integration_classes] - return integrations diff --git a/appdaemon/apps/controllerx/cx_core/integration/deconz.py b/appdaemon/apps/controllerx/cx_core/integration/deconz.py deleted file mode 100644 index 4db7ab8..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/deconz.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any - -from appdaemon.plugins.hass.hassapi import Hass -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - -LISTENS_TO_ID = "id" -LISTENS_TO_UNIQUE_ID = "unique_id" - - -class DeCONZIntegration(Integration): - name = "deconz" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_deconz_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - listens_to = self.kwargs.get("listen_to", LISTENS_TO_ID) - if listens_to not in (LISTENS_TO_ID, LISTENS_TO_UNIQUE_ID): - raise ValueError( - "`listens_to` for deCONZ integration should either be `id` or `unique_id`" - ) - await Hass.listen_event( - self.controller, - self.event_callback, - "deconz_event", - **{listens_to: controller_id} - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - type_ = self.kwargs.get("type", "event") - await self.controller.handle_action(data[type_], extra=data) diff --git a/appdaemon/apps/controllerx/cx_core/integration/event.py b/appdaemon/apps/controllerx/cx_core/integration/event.py deleted file mode 100644 index f5adb77..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/event.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Any - -from appdaemon.plugins.hass.hassapi import Hass -from cx_core.integration import EventData, Integration - - -class EventIntegration(Integration): - name = "event" - - def get_arg(self, arg: str) -> Any: - try: - return self.kwargs[arg] - except KeyError: - raise ValueError(f"{arg} is a mandatory field for event integration.") - - async def listen_changes(self, controller_id: str) -> None: - event_type: str = self.get_arg("event_type") - controller_key: str = self.get_arg("controller_key") - self.controller.log( - f"Listening to `{event_type}` events for controller `{controller_key}={controller_id}`" - ) - await Hass.listen_event( - self.controller, - self.event_callback, - event_type, - **{controller_key: controller_id}, - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - action_template: str = self.get_arg("action_template") - try: - action = action_template.format(**data) - except Exception: - self.controller.log( - f"Template `{action_template}` could not be rendered with data={data}", - level="WARNING", - ) - return - await self.controller.handle_action(action) diff --git a/appdaemon/apps/controllerx/cx_core/integration/homematic.py b/appdaemon/apps/controllerx/cx_core/integration/homematic.py deleted file mode 100644 index df1b388..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/homematic.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import TYPE_CHECKING, Any - -from appdaemon.plugins.hass.hassapi import Hass -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - -if TYPE_CHECKING: - from cx_core.controller import Controller - - -class HomematicIntegration(Integration): - name = "homematic" - _registered_controller_ids: set[str] - - def __init__(self, controller: "Controller", kwargs: dict[str, Any]): - self._registered_controller_ids = set() - super().__init__(controller, kwargs) - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_homematic_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - self._registered_controller_ids.add(controller_id) - await Hass.listen_event( - self.controller, self.event_callback, "homematic.keypress" - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - if data["name"] not in self._registered_controller_ids: - return - param = data["param"] - channel = data["channel"] - action = f"{param}_{channel}" - await self.controller.handle_action(action, extra=data) diff --git a/appdaemon/apps/controllerx/cx_core/integration/lutron_caseta.py b/appdaemon/apps/controllerx/cx_core/integration/lutron_caseta.py deleted file mode 100644 index 1620289..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/lutron_caseta.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any - -from appdaemon.plugins.hass.hassapi import Hass -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - - -class LutronIntegration(Integration): - name = "lutron_caseta" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_lutron_caseta_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - await Hass.listen_event( - self.controller, - self.event_callback, - "lutron_caseta_button_event", - serial=controller_id, - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> 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/appdaemon/apps/controllerx/cx_core/integration/mqtt.py b/appdaemon/apps/controllerx/cx_core/integration/mqtt.py deleted file mode 100644 index 91864bb..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/mqtt.py +++ /dev/null @@ -1,43 +0,0 @@ -import json -from typing import Any - -from appdaemon.plugins.mqtt.mqttapi import Mqtt -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - - -class MQTTIntegration(Integration): - name = "mqtt" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_z2m_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - await Mqtt.listen_event( - self.controller, self.event_callback, topic=controller_id, namespace="mqtt" - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - self.controller.log(f"MQTT data event: {data}", level="DEBUG") - payload_key = self.kwargs.get("key") - if "payload" not in data: - return - payload = data["payload"] - action_key: str - if payload_key is None: - action_key = payload - else: - try: - action_key = str(json.loads(payload)[payload_key]).lower() - except json.decoder.JSONDecodeError: - raise ValueError( - f"`key` is being used ({payload_key}). " - f"Following payload is not a valid JSON: {payload}" - ) - except KeyError: - raise ValueError( - f"Following payload does not contain `{payload_key}`: {payload}" - ) - await self.controller.handle_action(action_key) diff --git a/appdaemon/apps/controllerx/cx_core/integration/shelly.py b/appdaemon/apps/controllerx/cx_core/integration/shelly.py deleted file mode 100644 index 7fb1bea..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/shelly.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Any - -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) -> DefaultActionsMapping | None: - 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) diff --git a/appdaemon/apps/controllerx/cx_core/integration/shellyforhass.py b/appdaemon/apps/controllerx/cx_core/integration/shellyforhass.py deleted file mode 100644 index 35d57c2..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/shellyforhass.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any - -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) -> DefaultActionsMapping | None: - 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) diff --git a/appdaemon/apps/controllerx/cx_core/integration/state.py b/appdaemon/apps/controllerx/cx_core/integration/state.py deleted file mode 100644 index 3d3276b..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/state.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any - -from appdaemon.plugins.hass.hassapi import Hass -from cx_const import DefaultActionsMapping -from cx_core.integration import Integration - - -class StateIntegration(Integration): - name = "state" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_state_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - attribute = self.kwargs.get("attribute", None) - await Hass.listen_state( - self.controller, self.state_callback, controller_id, attribute=attribute - ) - - async def state_callback( - self, - entity: str | None, - attribute: str | None, - old: str | None, - new: str, - kwargs: dict[str, Any], - ) -> None: - await self.controller.handle_action(new, previous_state=old) diff --git a/appdaemon/apps/controllerx/cx_core/integration/tasmota.py b/appdaemon/apps/controllerx/cx_core/integration/tasmota.py deleted file mode 100644 index 02d0d94..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/tasmota.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -from typing import Any - -from appdaemon.plugins.mqtt.mqttapi import Mqtt -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - - -class TasmotaIntegration(Integration): - name = "tasmota" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_tasmota_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - component_key = self.kwargs.get("component") - if component_key is None: - raise ValueError( - "`component` attribute is mandatory. " - "Check example from https://xaviml.github.io/controllerx/start/integrations/tasmota" - ) - await Mqtt.listen_event( - self.controller, self.event_callback, topic=controller_id, namespace="mqtt" - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - self.controller.log(f"MQTT data event: {data}", level="DEBUG") - component_key: str = self.kwargs["component"] - payload_key: str = self.kwargs.get("key", "Action") - if "payload" not in data: - return - payload: str = data["payload"] - # Even though this checks if "compoenent_key" is in the payload - # (as string, not dictionary), it is preferred for its performance - if component_key not in payload: - return - try: - action_key = str(json.loads(payload)[component_key][payload_key]) - except json.decoder.JSONDecodeError: - raise ValueError( - f"`key` is being used ({payload_key}). " - f"Following payload is not a valid JSON: {payload}" - ) - except KeyError: - raise ValueError( - "Following payload does not contain " - f"payload_key=`{payload_key}`: {payload}" - ) - await self.controller.handle_action(action_key) diff --git a/appdaemon/apps/controllerx/cx_core/integration/z2m.py b/appdaemon/apps/controllerx/cx_core/integration/z2m.py deleted file mode 100644 index ad744fb..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/z2m.py +++ /dev/null @@ -1,85 +0,0 @@ -import json -from typing import Any - -from appdaemon.plugins.hass.hassapi import Hass -from appdaemon.plugins.mqtt.mqttapi import Mqtt -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - -LISTENS_TO_HA = "ha" -LISTENS_TO_MQTT = "mqtt" -LISTENS_TO_EVENT = "event" - - -class Z2MIntegration(Integration): - name = "z2m" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_z2m_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - listens_to = self.kwargs.get("listen_to", LISTENS_TO_HA) - if listens_to == LISTENS_TO_HA: - self.controller.log( - "⚠️ Listening to HA sensor actions is now deprecated and will be removed in the future. Use `listen_to: mqtt` or `listen_to: event` instead." - " Read more about it here: https://xaviml.github.io/controllerx/others/z2m-ha-sensor-deprecated", - level="WARNING", - ascii_encode=False, - ) - await Hass.listen_state(self.controller, self.state_callback, controller_id) - elif listens_to == LISTENS_TO_MQTT: - topic_prefix = self.kwargs.get("topic_prefix", "zigbee2mqtt") - await Mqtt.listen_event( - self.controller, - self.event_callback, - topic=f"{topic_prefix}/{controller_id}", - namespace="mqtt", - ) - elif listens_to == LISTENS_TO_EVENT: - await Hass.listen_state( - self.controller, - self.state_callback, - f"event.{controller_id}", - attribute="event_type", - ) - else: - raise ValueError( - "`listen_to` has to be either `ha`, `mqtt` or `event`. Default is `ha`." - ) - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - self.controller.log(f"MQTT data event: {data}", level="DEBUG") - action_key = self.kwargs.get("action_key", "action") - action_group_key = self.kwargs.get("action_group_key", "action_group") - if "payload" not in data: - return - payload = json.loads(data["payload"]) - if action_key not in payload: - self.controller.log( - f"⚠️ There is no `{action_key}` in the MQTT topic payload", - level="WARNING", - ascii_encode=False, - ) - return - if action_group_key in payload and "action_group" in self.kwargs: - action_group = self.controller.get_list(self.kwargs["action_group"]) - if payload["action_group"] not in action_group: - self.controller.log( - f"Action group {payload['action_group']} not found in " - f"action groups: {action_group}", - level="DEBUG", - ) - return - await self.controller.handle_action(payload[action_key], extra=payload) - - async def state_callback( - self, - entity: str | None, - attribute: str | None, - old: str | None, - new: str, - kwargs: dict[str, Any], - ) -> None: - await self.controller.handle_action(new, previous_state=old) diff --git a/appdaemon/apps/controllerx/cx_core/integration/zha.py b/appdaemon/apps/controllerx/cx_core/integration/zha.py deleted file mode 100644 index a02111e..0000000 --- a/appdaemon/apps/controllerx/cx_core/integration/zha.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Any - -from appdaemon.plugins.hass.hassapi import Hass -from cx_const import DefaultActionsMapping -from cx_core.integration import EventData, Integration - - -class ZHAIntegration(Integration): - name = "zha" - - def get_default_actions_mapping(self) -> DefaultActionsMapping | None: - return self.controller.get_zha_actions_mapping() - - async def listen_changes(self, controller_id: str) -> None: - await Hass.listen_event( - self.controller, self.event_callback, "zha_event", device_ieee=controller_id - ) - - def get_action(self, data: EventData) -> str: - command = data["command"] - args = data["args"] - if isinstance(args, dict): - args = args["args"] - args = list(map(str, args)) - action = command - if not (command == "stop" or command == "release"): - if len(args) > 0: - action += "_" + "_".join(args) - return action - - async def event_callback( - self, event_name: str, data: EventData, kwargs: dict[str, Any] - ) -> None: - action = self.controller.get_zha_action(data) - if action is None: - # If there is no action extracted from the controller then - # we extract with the standard function - try: - action = self.get_action(data) - except Exception: - self.controller.log( - f"The following event could not be parsed: {data}", level="WARNING" - ) - return - - await self.controller.handle_action(action, extra=data) diff --git a/appdaemon/apps/controllerx/cx_core/release_hold_controller.py b/appdaemon/apps/controllerx/cx_core/release_hold_controller.py deleted file mode 100644 index e016617..0000000 --- a/appdaemon/apps/controllerx/cx_core/release_hold_controller.py +++ /dev/null @@ -1,69 +0,0 @@ -import abc -from typing import Any - -from cx_core.controller import Controller, action - -DEFAULT_DELAY = 350 # In milliseconds -DEFAULT_RELEASE_DELAY = 0 # In seconds - - -class ReleaseHoldController(Controller, abc.ABC): - DEFAULT_MAX_LOOPS = 50 - - on_hold: bool - delay: float - max_loops: int - hold_release_toggle: bool - release_delay: int - - async def init(self) -> None: - self.on_hold = False - self.delay = self.args.get("delay", self.default_delay()) - self.max_loops = self.args.get( - "max_loops", ReleaseHoldController.DEFAULT_MAX_LOOPS - ) - self.hold_release_toggle = self.args.get("hold_release_toggle", False) - self.release_delay = self.args.get("release_delay", DEFAULT_RELEASE_DELAY) - await super().init() - - @action - async def release(self) -> None: - if self.release_delay > 0: - await self.sleep(self.release_delay) - self.on_hold = False - - async def hold(self, *args: Any) -> None: - loops = 0 - self.on_hold = True - stop = False - while self.on_hold and not stop: - stop = await self.hold_loop(*args) - # Stop the iteration if we either stop from the hold_loop - # or we reached the max loop number - stop = stop or loops >= self.max_loops - await self.sleep(self.delay / 1000) - loops += 1 - self.on_hold = False - - async def before_action(self, action: str, *args: Any, **kwargs: Any) -> bool: - super_before_action = await super().before_action(action, *args, **kwargs) - to_return = not (action == "hold" and self.on_hold) - if action == "hold" and self.on_hold and self.hold_release_toggle: - self.on_hold = False - return super_before_action and to_return - - @abc.abstractmethod - async def hold_loop(self, *args: Any) -> bool: - """ - This function is called by the ReleaseHoldController depending on the settings. - It stops calling the function once release action is called or when this function - returns True. - """ - raise NotImplementedError - - def default_delay(self) -> int: - """ - This function can be overwritten for each device to indeicate the delay - for the specific device, by default it returns the default delay from the app - """ - return DEFAULT_DELAY diff --git a/appdaemon/apps/controllerx/cx_core/stepper/__init__.py b/appdaemon/apps/controllerx/cx_core/stepper/__init__.py deleted file mode 100644 index 1d34e59..0000000 --- a/appdaemon/apps/controllerx/cx_core/stepper/__init__.py +++ /dev/null @@ -1,112 +0,0 @@ -import abc -from dataclasses import dataclass - -from cx_const import Number, StepperDir - - -class MinMax: - def __init__(self, min: Number, max: Number, margin: float = 0.05) -> None: - self._min = min - self._max = max - self.margin_dist = (max - min) * margin - - @property - def min(self) -> Number: - return self._min - - @property - def max(self) -> Number: - return self._max - - def is_min(self, value: Number) -> bool: - return self._min == value - - def is_max(self, value: Number) -> bool: - return self._max == value - - def is_between(self, value: Number) -> bool: - return self._min < value < self._max - - def in_min_boundaries(self, value: Number) -> bool: - return self._min <= value <= (self._min + self.margin_dist) - - def in_max_boundaries(self, value: Number) -> bool: - return (self._max - self.margin_dist) <= value <= self._max - - def clip(self, value: Number) -> Number: - return max(self._min, min(value, self._max)) - - def __repr__(self) -> str: - return f"MinMax({self.min}, {self.max})" - - -@dataclass -class StepperOutput: - next_value: Number - next_direction: str | None - - @property - def exceeded(self) -> bool: - return self.next_direction is None - - -class Stepper(abc.ABC): - sign_mapping = {StepperDir.UP: 1, StepperDir.DOWN: -1} - - min_max: MinMax - steps: Number - previous_direction: str - relative_steps: bool - - @staticmethod - def invert_direction(direction: str) -> str: - return StepperDir.UP if direction == StepperDir.DOWN else StepperDir.DOWN - - @staticmethod - def sign(direction: str) -> int: - return Stepper.sign_mapping[direction] - - @staticmethod - def apply_sign(value: Number, direction: str) -> Number: - return Stepper.sign(direction) * value - - def __init__( - self, - min_max: MinMax, - steps: Number, - previous_direction: str = StepperDir.DOWN, - relative_steps: bool = True, - ) -> None: - self.min_max = min_max - self.steps = steps - self.previous_direction = previous_direction - self.relative_steps = relative_steps - - def _compute_step(self) -> float: - if self.relative_steps: - max_ = self.min_max.max - min_ = self.min_max.min - return (max_ - min_) / self.steps - else: - return self.steps - - def get_direction(self, value: Number, direction: str) -> str: - if direction == StepperDir.TOGGLE: - direction = Stepper.invert_direction(self.previous_direction) - self.previous_direction = direction - return direction - - @abc.abstractmethod - def step(self, value: Number, direction: str) -> StepperOutput: - """ - This function updates the value according to the steps - that needs to take and returns the new value together with - the new direction it will need to go. If next_direction is - None, the loop will stop executing. - """ - raise NotImplementedError - - -class InvertStepper(Stepper): - def step(self, value: Number, direction: str) -> StepperOutput: - return StepperOutput(self.apply_sign(value, direction), next_direction=None) diff --git a/appdaemon/apps/controllerx/cx_core/stepper/bounce_stepper.py b/appdaemon/apps/controllerx/cx_core/stepper/bounce_stepper.py deleted file mode 100644 index ba87310..0000000 --- a/appdaemon/apps/controllerx/cx_core/stepper/bounce_stepper.py +++ /dev/null @@ -1,17 +0,0 @@ -from cx_const import Number -from cx_core.stepper import Stepper, StepperOutput - - -class BounceStepper(Stepper): - def step(self, value: Number, direction: str) -> StepperOutput: - value = self.min_max.clip(value) - step = self._compute_step() - - new_value = value + Stepper.apply_sign(step, direction) - if self.min_max.is_between(new_value): - return StepperOutput(round(new_value, 3), next_direction=direction) - else: - new_value = 2 * self.min_max.clip(new_value) - new_value - return StepperOutput( - round(new_value, 3), next_direction=Stepper.invert_direction(direction) - ) diff --git a/appdaemon/apps/controllerx/cx_core/stepper/index_loop_stepper.py b/appdaemon/apps/controllerx/cx_core/stepper/index_loop_stepper.py deleted file mode 100644 index 78aa6c6..0000000 --- a/appdaemon/apps/controllerx/cx_core/stepper/index_loop_stepper.py +++ /dev/null @@ -1,31 +0,0 @@ -from cx_const import Number, StepperDir -from cx_core.stepper import MinMax, Stepper, StepperOutput - - -class IndexLoopStepper(Stepper): - def __init__( - self, - size: int, - previous_direction: str = StepperDir.DOWN, - relative_steps: bool = True, - ) -> None: - super().__init__( - MinMax(0, size - 1), - size, - previous_direction, - relative_steps, - ) - - def step(self, value: Number, direction: str) -> StepperOutput: - value = self.min_max.clip(value) - sign = self.sign(direction) - # We add +1 to make the max be included - max_ = int(self.min_max.max) + 1 - min_ = int(self.min_max.min) - if self.relative_steps: - step = (max_ - min_) // self.steps - else: - step = self.steps - - new_value = (int(value) + step * sign) % (max_ - min_) + min_ - return StepperOutput(new_value, next_direction=direction) diff --git a/appdaemon/apps/controllerx/cx_core/stepper/loop_stepper.py b/appdaemon/apps/controllerx/cx_core/stepper/loop_stepper.py deleted file mode 100644 index 1a66cb0..0000000 --- a/appdaemon/apps/controllerx/cx_core/stepper/loop_stepper.py +++ /dev/null @@ -1,16 +0,0 @@ -from cx_const import Number -from cx_core.stepper import Stepper, StepperOutput - - -class LoopStepper(Stepper): - def step(self, value: Number, direction: str) -> StepperOutput: - value = self.min_max.clip(value) - max_ = self.min_max.max - min_ = self.min_max.min - step = self._compute_step() - - new_value = ( - ((value + Stepper.apply_sign(step, direction)) - min_) % (max_ - min_) - ) + min_ - new_value = round(new_value, 3) - return StepperOutput(new_value, next_direction=direction) diff --git a/appdaemon/apps/controllerx/cx_core/stepper/stop_stepper.py b/appdaemon/apps/controllerx/cx_core/stepper/stop_stepper.py deleted file mode 100644 index b8047f8..0000000 --- a/appdaemon/apps/controllerx/cx_core/stepper/stop_stepper.py +++ /dev/null @@ -1,26 +0,0 @@ -from cx_const import Number, StepperDir -from cx_core.stepper import Stepper, StepperOutput - - -class StopStepper(Stepper): - def get_direction(self, value: Number, direction: str) -> str: - value = self.min_max.clip(value) - if direction == StepperDir.TOGGLE and self.min_max.in_min_boundaries(value): - self.previous_direction = StepperDir.UP - return self.previous_direction - if direction == StepperDir.TOGGLE and self.min_max.in_max_boundaries(value): - self.previous_direction = StepperDir.DOWN - return self.previous_direction - return super().get_direction(value, direction) - - def step(self, value: Number, direction: str) -> StepperOutput: - value = self.min_max.clip(value) - step = self._compute_step() - - new_value = value + Stepper.apply_sign(step, direction) - new_value = round(new_value, 3) - if self.min_max.is_between(new_value): - return StepperOutput(new_value, next_direction=direction) - else: - new_value = self.min_max.clip(new_value) - return StepperOutput(new_value, next_direction=None) diff --git a/appdaemon/apps/controllerx/cx_core/type/__init__.py b/appdaemon/apps/controllerx/cx_core/type/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/appdaemon/apps/controllerx/cx_core/type/cover_controller.py b/appdaemon/apps/controllerx/cx_core/type/cover_controller.py deleted file mode 100644 index 677c4fa..0000000 --- a/appdaemon/apps/controllerx/cx_core/type/cover_controller.py +++ /dev/null @@ -1,125 +0,0 @@ -from collections.abc import Awaitable, Callable -from typing import Any - -from cx_const import Cover, PredefinedActionsMapping -from cx_core.controller import action -from cx_core.feature_support.cover import CoverSupport -from cx_core.type_controller import Entity, TypeController - - -class CoverController(TypeController[Entity]): - """ - This is the main class that controls the coveres for different devices. - Type of actions: - - Open - - Close - Parameters taken: - - controller (required): Inherited from Controller - - cover (required): cover entity name - - open_position (optional): The open position. Default is 100 - - close_position (optional): The close position. Default is 0 - """ - - domains = ["cover"] - entity_arg = "cover" - - open_position: int - close_position: int - - cover_duration: int | None - - is_supposedly_moving: bool = False - stop_timer_handle: str | None = None - - async def init(self) -> None: - self.open_position = self.args.get("open_position", 100) - self.close_position = self.args.get("close_position", 0) - self.cover_duration = self.args.get("cover_duration") - if self.open_position < self.close_position: - raise ValueError("`open_position` must be higher than `close_position`") - await super().init() - - def _get_entity_type(self) -> type[Entity]: - return Entity - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - return { - Cover.OPEN: self.open, - Cover.CLOSE: self.close, - Cover.STOP: self.stop, - Cover.TOGGLE_OPEN: (self.toggle, (self.open,)), - Cover.TOGGLE_CLOSE: (self.toggle, (self.close,)), - } - - async def cover_stopped_cb(self, kwargs: dict[str, Any]) -> None: - self.is_supposedly_moving = False - self.stop_timer_handle = None - - async def start_timer(self) -> None: - if self.cover_duration is None: - return - await self.stop_timer() - self.is_supposedly_moving = True - self.stop_timer_handle = await self.run_in( - self.cover_stopped_cb, self.cover_duration - ) - - async def stop_timer(self) -> None: - if self.stop_timer_handle is not None: - self.is_supposedly_moving = False - await self.cancel_timer(self.stop_timer_handle) - - @action - async def open(self) -> None: - if await self.feature_support.is_supported(CoverSupport.SET_COVER_POSITION): - await self.call_service( - "cover/set_cover_position", - entity_id=self.entity.name, - position=self.open_position, - ) - elif await self.feature_support.is_supported(CoverSupport.OPEN): - await self.call_service("cover/open_cover", entity_id=self.entity.name) - else: - self.log( - f"⚠️ `{self.entity}` does not support SET_COVER_POSITION or OPEN", - level="WARNING", - ascii_encode=False, - ) - return - await self.start_timer() - - @action - async def close(self) -> None: - if await self.feature_support.is_supported(CoverSupport.SET_COVER_POSITION): - await self.call_service( - "cover/set_cover_position", - entity_id=self.entity.name, - position=self.close_position, - ) - elif await self.feature_support.is_supported(CoverSupport.CLOSE): - await self.call_service("cover/close_cover", entity_id=self.entity.name) - else: - self.log( - f"⚠️ `{self.entity}` does not support SET_COVER_POSITION or CLOSE", - level="WARNING", - ascii_encode=False, - ) - return - await self.start_timer() - - @action - async def stop(self) -> None: - await self.stop_timer() - await self.call_service("cover/stop_cover", entity_id=self.entity.name) - - @action - async def toggle(self, action: Callable[[], Awaitable[None]]) -> None: - cover_state = await self.get_entity_state() - if ( - cover_state == "opening" - or cover_state == "closing" - or self.is_supposedly_moving - ): - await self.stop() - else: - await action() diff --git a/appdaemon/apps/controllerx/cx_core/type/light_controller.py b/appdaemon/apps/controllerx/cx_core/type/light_controller.py deleted file mode 100644 index d6b5a37..0000000 --- a/appdaemon/apps/controllerx/cx_core/type/light_controller.py +++ /dev/null @@ -1,958 +0,0 @@ -import asyncio -from functools import lru_cache -from typing import Any, Literal - -from cx_const import Light, Number, PredefinedActionsMapping, StepperDir, StepperMode -from cx_core.color_helper import Color, get_color_wheel -from cx_core.controller import action -from cx_core.feature_support.light import LightSupport -from cx_core.integration import EventData -from cx_core.integration.deconz import DeCONZIntegration -from cx_core.integration.z2m import Z2MIntegration -from cx_core.integration.zha import ZHAIntegration -from cx_core.release_hold_controller import ReleaseHoldController -from cx_core.stepper import MinMax, Stepper -from cx_core.stepper.bounce_stepper import BounceStepper -from cx_core.stepper.index_loop_stepper import IndexLoopStepper -from cx_core.stepper.loop_stepper import LoopStepper -from cx_core.stepper.stop_stepper import StopStepper -from cx_core.type_controller import Entity, TypeController - -DEFAULT_MANUAL_STEPS = 10 -DEFAULT_AUTOMATIC_STEPS = 10 -DEFAULT_MIN_BRIGHTNESS = 1 -DEFAULT_MAX_BRIGHTNESS = 255 -DEFAULT_MIN_WHITE_VALUE = 1 -DEFAULT_MAX_WHITE_VALUE = 255 -DEFAULT_MIN_COLOR_TEMP = 153 -DEFAULT_MAX_COLOR_TEMP = 500 -DEFAULT_COLOR_TEMP = ( - DEFAULT_MAX_COLOR_TEMP - DEFAULT_MIN_COLOR_TEMP -) // 2 + DEFAULT_MIN_COLOR_TEMP -DEFAULT_TRANSITION = 300 -DEFAULT_ADD_TRANSITION = True -DEFAULT_TRANSITION_TURN_TOGGLE = False -DEFAULT_HOLD_TOGGLE_DIRECTION_INIT = "up" - -ColorMode = Literal["auto", "xy_color", "color_temp"] - -COLOR_MODES = {"hs", "xy", "rgb", "rgbw", "rgbww"} -STEPPER_MODES: dict[str, type[Stepper]] = { - StepperMode.STOP: StopStepper, - StepperMode.LOOP: LoopStepper, - StepperMode.BOUNCE: BounceStepper, -} - - -class LightEntity(Entity): - color_mode: ColorMode - - def __init__( - self, - name: str, - entities: list[str] | None = None, - color_mode: ColorMode = "auto", - ) -> None: - super().__init__(name, entities) - self.color_mode = color_mode - - -class LightController(TypeController[LightEntity], ReleaseHoldController): - """ - This is the main class that controls the lights for different devices. - Type of actions: - - On/Off/Toggle - - Brightness click and hold - - Color temperature click and hold - - xy color click and hold - If a light supports xy_color and color_temperature, then xy_color will be the - default functionality. Parameters taken: - - controller (required): Inherited from Controller - - light (required): This is either the light entity name or a dictionary as - {name: string, color_mode: auto | xy_color | color_temp} - - delay (optional): Inherited from ReleaseHoldController - - manual_steps (optional): Number of steps to go from min to max when clicking. - - automatic_steps (optional): Number of steps to go from min to max when smoothing. - """ - - ATTRIBUTE_BRIGHTNESS = "brightness" - ATTRIBUTE_WHITE_VALUE = "white_value" - # With the following attribute, it will select color_temp or xy_color, depending on the light. - ATTRIBUTE_COLOR = "color" - ATTRIBUTE_COLOR_TEMP = "color_temp" - ATTRIBUTE_XY_COLOR = "xy_color" - - ATTRIBUTES_LIST = [ - ATTRIBUTE_BRIGHTNESS, - ATTRIBUTE_WHITE_VALUE, - ATTRIBUTE_COLOR, - ATTRIBUTE_COLOR_TEMP, - ATTRIBUTE_XY_COLOR, - ] - - index_color = 0 - value_attribute = None - - # These are intermediate variables to store the checked value - smooth_power_on_check: bool - remove_transition_check: bool - next_direction: str | None = None - - manual_steps: Number - automatic_steps: Number - min_max_attributes: dict[str, MinMax] - - domains = ["light"] - entity_arg = "light" - - _supported_color_modes: set[str] | None - - async def init(self) -> None: - self.manual_steps = self.args.get("manual_steps", DEFAULT_MANUAL_STEPS) - self.automatic_steps = self.args.get("automatic_steps", DEFAULT_AUTOMATIC_STEPS) - - self.min_max_attributes = { - self.ATTRIBUTE_BRIGHTNESS: MinMax( - self.args.get("min_brightness", DEFAULT_MIN_BRIGHTNESS), - self.args.get("max_brightness", DEFAULT_MAX_BRIGHTNESS), - ), - self.ATTRIBUTE_WHITE_VALUE: MinMax( - self.args.get("min_white_value", DEFAULT_MIN_WHITE_VALUE), - self.args.get("max_white_value", DEFAULT_MAX_WHITE_VALUE), - ), - self.ATTRIBUTE_COLOR_TEMP: MinMax( - self.args.get("min_color_temp", DEFAULT_MIN_COLOR_TEMP), - self.args.get("max_color_temp", DEFAULT_MAX_COLOR_TEMP), - ), - } - - self.transition = self.args.get("transition", DEFAULT_TRANSITION) - self.color_wheel = get_color_wheel( - self.args.get("color_wheel", "default_color_wheel") - ) - self._supported_color_modes = self.args.get("supported_color_modes") - - self.smooth_power_on = self.args.get( - "smooth_power_on", self.supports_smooth_power_on() - ) - self.add_transition = self.args.get("add_transition", DEFAULT_ADD_TRANSITION) - self.add_transition_turn_toggle = self.args.get( - "add_transition_turn_toggle", DEFAULT_TRANSITION_TURN_TOGGLE - ) - self.hold_toggle_direction_init = self.get_option( - self.args.get( - "hold_toggle_direction_init", DEFAULT_HOLD_TOGGLE_DIRECTION_INIT - ), - [StepperDir.UP, StepperDir.DOWN], - "`hold_toggle_direction_init`", - ) - await super().init() - - def _get_entity_type(self) -> type[LightEntity]: - return LightEntity - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - return { - Light.ON: self.on, - Light.OFF: self.off, - Light.TOGGLE: self.toggle, - Light.TOGGLE_FULL_BRIGHTNESS: ( - self.toggle_full, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - Light.TOGGLE_FULL_WHITE_VALUE: ( - self.toggle_full, - (LightController.ATTRIBUTE_WHITE_VALUE,), - ), - Light.TOGGLE_FULL_COLOR_TEMP: ( - self.toggle_full, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.TOGGLE_MIN_BRIGHTNESS: ( - self.toggle_min, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - Light.TOGGLE_MIN_WHITE_VALUE: ( - self.toggle_min, - (LightController.ATTRIBUTE_WHITE_VALUE,), - ), - Light.TOGGLE_MIN_COLOR_TEMP: ( - self.toggle_min, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.RELEASE: self.release, - Light.ON_FULL_BRIGHTNESS: ( - self.on_full, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - Light.ON_FULL_WHITE_VALUE: ( - self.on_full, - (LightController.ATTRIBUTE_WHITE_VALUE,), - ), - Light.ON_FULL_COLOR_TEMP: ( - self.on_full, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.ON_MIN_BRIGHTNESS: ( - self.on_min, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - Light.ON_MIN_WHITE_VALUE: ( - self.on_min, - (LightController.ATTRIBUTE_WHITE_VALUE,), - ), - Light.ON_MIN_COLOR_TEMP: ( - self.on_min, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.ON_MIN_MAX_BRIGHTNESS: ( - self.on_min_max, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - Light.ON_MAX_MIN_BRIGHTNESS: ( - self.on_max_min, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - Light.ON_MIN_MAX_COLOR_TEMP: ( - self.on_min_max, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.ON_MAX_MIN_COLOR_TEMP: ( - self.on_max_min, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.SET_HALF_BRIGHTNESS: ( - self.set_value, - ( - LightController.ATTRIBUTE_BRIGHTNESS, - 0.5, - ), - ), - Light.SET_HALF_WHITE_VALUE: ( - self.set_value, - ( - LightController.ATTRIBUTE_WHITE_VALUE, - 0.5, - ), - ), - Light.SET_HALF_COLOR_TEMP: ( - self.set_value, - ( - LightController.ATTRIBUTE_COLOR_TEMP, - 0.5, - ), - ), - Light.SYNC: self.sync, - Light.CLICK: self.click, - Light.CLICK_BRIGHTNESS_UP: ( - self.click, - ( - LightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.UP, - ), - ), - Light.CLICK_BRIGHTNESS_DOWN: ( - self.click, - ( - LightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.DOWN, - ), - ), - Light.CLICK_WHITE_VALUE_UP: ( - self.click, - ( - LightController.ATTRIBUTE_WHITE_VALUE, - StepperDir.UP, - ), - ), - Light.CLICK_WHITE_VALUE_DOWN: ( - self.click, - ( - LightController.ATTRIBUTE_WHITE_VALUE, - StepperDir.DOWN, - ), - ), - Light.CLICK_COLOR_UP: ( - self.click, - ( - LightController.ATTRIBUTE_COLOR, - StepperDir.UP, - ), - ), - Light.CLICK_COLOR_DOWN: ( - self.click, - ( - LightController.ATTRIBUTE_COLOR, - StepperDir.DOWN, - ), - ), - Light.CLICK_COLOR_TEMP_UP: ( - self.click, - ( - LightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.UP, - ), - ), - Light.CLICK_COLOR_TEMP_DOWN: ( - self.click, - ( - LightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.DOWN, - ), - ), - Light.CLICK_XY_COLOR_UP: ( - self.click, - ( - LightController.ATTRIBUTE_XY_COLOR, - StepperDir.UP, - ), - ), - Light.CLICK_XY_COLOR_DOWN: ( - self.click, - ( - LightController.ATTRIBUTE_XY_COLOR, - StepperDir.DOWN, - ), - ), - Light.HOLD: self.hold, - Light.HOLD_BRIGHTNESS_UP: ( - self.hold, - ( - LightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.UP, - ), - ), - Light.HOLD_BRIGHTNESS_DOWN: ( - self.hold, - ( - LightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.DOWN, - ), - ), - Light.HOLD_BRIGHTNESS_TOGGLE: ( - self.hold, - ( - LightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.TOGGLE, - ), - ), - Light.HOLD_WHITE_VALUE_UP: ( - self.hold, - ( - LightController.ATTRIBUTE_WHITE_VALUE, - StepperDir.UP, - ), - ), - Light.HOLD_WHITE_VALUE_DOWN: ( - self.hold, - ( - LightController.ATTRIBUTE_WHITE_VALUE, - StepperDir.DOWN, - ), - ), - Light.HOLD_WHITE_VALUE_TOGGLE: ( - self.hold, - ( - LightController.ATTRIBUTE_WHITE_VALUE, - StepperDir.TOGGLE, - ), - ), - Light.HOLD_COLOR_UP: ( - self.hold, - ( - LightController.ATTRIBUTE_COLOR, - StepperDir.UP, - ), - ), - Light.HOLD_COLOR_DOWN: ( - self.hold, - ( - LightController.ATTRIBUTE_COLOR, - StepperDir.DOWN, - ), - ), - Light.HOLD_COLOR_TOGGLE: ( - self.hold, - ( - LightController.ATTRIBUTE_COLOR, - StepperDir.TOGGLE, - ), - ), - Light.HOLD_COLOR_TEMP_UP: ( - self.hold, - ( - LightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.UP, - ), - ), - Light.HOLD_COLOR_TEMP_DOWN: ( - self.hold, - ( - LightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.DOWN, - ), - ), - Light.HOLD_COLOR_TEMP_TOGGLE: ( - self.hold, - ( - LightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.TOGGLE, - ), - ), - Light.HOLD_XY_COLOR_UP: ( - self.hold, - ( - LightController.ATTRIBUTE_XY_COLOR, - StepperDir.UP, - ), - ), - Light.HOLD_XY_COLOR_DOWN: ( - self.hold, - ( - LightController.ATTRIBUTE_XY_COLOR, - StepperDir.DOWN, - ), - ), - Light.HOLD_XY_COLOR_TOGGLE: ( - self.hold, - ( - LightController.ATTRIBUTE_XY_COLOR, - StepperDir.TOGGLE, - ), - ), - Light.XYCOLOR_FROM_CONTROLLER: self.xycolor_from_controller, - Light.COLORTEMP_FROM_CONTROLLER: self.colortemp_from_controller, - Light.COLORTEMP_FROM_CONTROLLER_STEP: ( - self.attribute_from_controller_step, - (LightController.ATTRIBUTE_COLOR_TEMP,), - ), - Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL: self.brightness_from_controller_level, - Light.BRIGHTNESS_FROM_CONTROLLER_ANGLE: self.brightness_from_controller_angle, - Light.BRIGHTNESS_FROM_CONTROLLER_STEP: ( - self.attribute_from_controller_step, - (LightController.ATTRIBUTE_BRIGHTNESS,), - ), - } - - async def check_remove_transition(self, on_from_user: bool) -> bool: - return ( - not self.add_transition - or (on_from_user and not self.add_transition_turn_toggle) - or await self.feature_support.not_supported(LightSupport.TRANSITION) - ) - - async def call_light_service( - self, service: str, force_transition: bool = False, **attributes: Any - ) -> None: - if "transition" not in attributes: - attributes["transition"] = self.transition / 1000 - if self.remove_transition_check and not force_transition: - del attributes["transition"] - await self.call_service(service, entity_id=self.entity.name, **attributes) - - async def _on(self, **attributes: Any) -> None: - await self.call_light_service("light/turn_on", **attributes) - - @action - async def on(self, attributes: dict[str, float] | None = None) -> None: - attributes = {} if attributes is None else attributes - await self._on(**attributes) - - async def _off(self, **attributes: Any) -> None: - await self.call_light_service("light/turn_off", **attributes) - - @action - async def off(self) -> None: - await self._off() - - async def _toggle(self, **attributes: Any) -> None: - await self.call_light_service("light/toggle", **attributes) - - @action - async def toggle(self, attributes: dict[str, float] | None = None) -> None: - attributes = {} if attributes is None else attributes - await self._toggle(**attributes) - - async def _set_value(self, attribute: str, fraction: float) -> None: - fraction = max(0, min(fraction, 1)) - min_ = self.min_max_attributes[attribute].min - max_ = self.min_max_attributes[attribute].max - value = (max_ - min_) * fraction + min_ - await self._on(**{attribute: value}) - - @action - async def set_value(self, attribute: str, fraction: float) -> None: - await self._set_value(attribute, fraction) - - @action - async def toggle_full(self, attribute: str) -> None: - await self._toggle(**{attribute: self.min_max_attributes[attribute].max}) - - @action - async def toggle_min(self, attribute: str) -> None: - await self._toggle(**{attribute: self.min_max_attributes[attribute].min}) - - async def _on_full(self, attribute: str) -> None: - await self._set_value(attribute, 1) - - @action - async def on_full(self, attribute: str) -> None: - await self._on_full(attribute) - - async def _on_min(self, attribute: str) -> None: - await self._set_value(attribute, 0) - - @action - async def on_min(self, attribute: str) -> None: - await self._on_min(attribute) - - @action - async def on_min_max(self, attribute: str) -> None: - min_ = self.min_max_attributes[attribute].min - max_ = self.min_max_attributes[attribute].max - await self._on_min_max(attribute, default=min_, other=max_) - - @action - async def on_max_min(self, attribute: str) -> None: - min_ = self.min_max_attributes[attribute].min - max_ = self.min_max_attributes[attribute].max - await self._on_min_max(attribute, default=max_, other=min_) - - async def _on_min_max( - self, attribute: str, *, default: Number, other: Number - ) -> None: - light_state: str - attribute_value: Number - light_state, attribute_value = await asyncio.gather( - self.get_entity_state(), self.get_entity_state(attribute=attribute) - ) - - if light_state == "off" or attribute_value != default: - await self._on(**{attribute: default}) - else: - await self._on(**{attribute: other}) - - @action - async def sync( - self, - brightness: int | None = None, - color_temp: int = 370, # 2700K light - xy_color: Color = (0.323, 0.329), # white colour - ) -> None: - attributes: dict[Any, Any] = {} - try: - color_attribute = await self.get_attribute(LightController.ATTRIBUTE_COLOR) - if color_attribute == LightController.ATTRIBUTE_COLOR_TEMP: - attributes[color_attribute] = color_temp - else: - attributes[color_attribute] = list(xy_color) - except ValueError: - self.log( - "⚠️ `sync` action will only change brightness", - level="WARNING", - ascii_encode=False, - ) - await self._on( - **attributes, - brightness=( - brightness - or self.min_max_attributes[LightController.ATTRIBUTE_BRIGHTNESS].max - ), - ) - - @action - async def xycolor_from_controller(self, extra: EventData | None = None) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_color" not in extra: - self.log( - "`action_color` is not present in the MQTT payload", level="WARNING" - ) - return - xy_color = extra["action_color"] - await self._on(xy_color=[xy_color["x"], xy_color["y"]]) - elif isinstance(self.integration, DeCONZIntegration): - if "xy" not in extra: - self.log("`xy` is not present in the deCONZ event", level="WARNING") - return - await self._on(xy_color=list(extra["xy"])) - - @action - async def colortemp_from_controller(self, extra: EventData | None = None) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_color_temperature" not in extra: - self.log( - "`action_color_temperature` is not present in the MQTT payload", - level="WARNING", - ) - return - await self._on(color_temp=extra["action_color_temperature"]) - - @action - async def brightness_from_controller_level( - self, extra: EventData | None = None - ) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_level" not in extra: - self.log( - "`action_level` is not present in the MQTT payload", - level="WARNING", - ) - return - await self._on(brightness=extra["action_level"]) - - @action - async def attribute_from_controller_step( - self, attribute: str, extra: EventData | None = None - ) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, ZHAIntegration): - try: - step_mode: int = extra["params"]["step_mode"] - step_size: int = extra["params"]["step_size"] - stepper = self.generate_stepper( - attribute, step_size, StepperMode.STOP, relative_steps=False - ) - direction = StepperDir.UP - if ( - attribute == LightController.ATTRIBUTE_BRIGHTNESS and step_mode == 1 - ) or ( - attribute == LightController.ATTRIBUTE_COLOR_TEMP and step_mode == 3 - ): - direction = StepperDir.DOWN - - # Transition time in seconds - transition_time: int = extra["params"]["transition_time"] - self.value_attribute = await self.get_value_attribute(attribute) - extra_attributes = { - "transition": transition_time, - "force_transition": True, - } - await self.change_light_state( - self.value_attribute, - attribute, - direction, - stepper, - extra_attributes=extra_attributes, - ) - except (KeyError, TypeError): - self.log( - "`params` should be present in ZHA event with " - "`step_mode`, `step_size`, and `transition_time`", - level="WARNING", - ) - - @action - async def brightness_from_controller_angle( - self, - mode: str = StepperMode.STOP, - steps: Number | None = None, - extra: EventData | None = None, - ) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_rotation_angle" not in extra: - self.log( - "`action_rotation_angle` is not present in the MQTT payload", - level="WARNING", - ) - return - angle = extra["action_rotation_angle"] - direction = StepperDir.UP if angle > 0 else StepperDir.DOWN - await self._hold( - LightController.ATTRIBUTE_BRIGHTNESS, direction, mode=mode, steps=steps - ) - - @property - async def supported_color_modes(self) -> set[str]: - if self._supported_color_modes is None or self.update_supported_features: - supported_color_modes: list[str] = await self.get_entity_state( - attribute="supported_color_modes" - ) - if supported_color_modes is not None: - self._supported_color_modes = set(supported_color_modes) - else: - raise ValueError( - f"`supported_color_modes` could not be read from `{self.entity}`. " - "Entity might not be available." - ) - - return self._supported_color_modes - - async def is_color_supported(self) -> bool: - return len(COLOR_MODES.intersection(await self.supported_color_modes)) > 0 - - async def is_colortemp_supported(self) -> bool: - return "color_temp" in await self.supported_color_modes - - def generate_stepper( - self, attribute: str, steps: Number, mode: str, *, relative_steps: bool = True - ) -> Stepper: - previous_direction = Stepper.invert_direction(self.hold_toggle_direction_init) - if attribute == LightController.ATTRIBUTE_XY_COLOR: - return IndexLoopStepper( - len(self.color_wheel), - previous_direction, - relative_steps, - ) - if mode not in STEPPER_MODES: - raise ValueError( - f"`{mode}` mode is not available. Options are: {list(STEPPER_MODES.keys())}" - ) - stepper_cls = STEPPER_MODES[mode] - return stepper_cls( - self.min_max_attributes[attribute], - steps, - previous_direction, - relative_steps, - ) - - @lru_cache(maxsize=None) - def get_stepper( - self, attribute: str, steps: Number, mode: str, *, tag: str - ) -> Stepper: - return self.generate_stepper(attribute, steps, mode) - - async def get_attribute(self, attribute: str) -> str: - if attribute == LightController.ATTRIBUTE_COLOR: - if self.entity.color_mode == "auto": - if await self.is_color_supported(): - return LightController.ATTRIBUTE_XY_COLOR - elif await self.is_colortemp_supported(): - return LightController.ATTRIBUTE_COLOR_TEMP - else: - raise ValueError( - "This light does not support xy_color or color_temp" - ) - else: - return self.entity.color_mode - else: - return attribute - - async def get_value_attribute(self, attribute: str) -> Number: - if self.smooth_power_on_check: - return 0 - if attribute == LightController.ATTRIBUTE_XY_COLOR: - return 0 - elif ( - attribute == LightController.ATTRIBUTE_BRIGHTNESS - or attribute == LightController.ATTRIBUTE_WHITE_VALUE - or attribute == LightController.ATTRIBUTE_COLOR_TEMP - ): - value = await self.get_entity_state(attribute=attribute) - if value is None and attribute == LightController.ATTRIBUTE_COLOR_TEMP: - return float(DEFAULT_COLOR_TEMP) - elif value is None and attribute != LightController.ATTRIBUTE_COLOR_TEMP: - raise ValueError( - f"Value for `{attribute}` attribute could not be retrieved " - f"from `{self.entity.main}`. " - "Check the FAQ to know more about this error: " - "https://xaviml.github.io/controllerx/faq" - ) - else: - try: - return float(value) - except ValueError: - raise ValueError( - f"Attribute `{attribute}` with `{value}` as a value " - "could not be converted to float" - ) - else: - raise ValueError(f"Attribute `{attribute}` not expected") - - def check_smooth_power_on( - self, attribute: str, direction: str, light_state: str - ) -> bool: - return ( - direction != StepperDir.DOWN - and attribute == self.ATTRIBUTE_BRIGHTNESS - and self.smooth_power_on - and light_state == "off" - ) - - async def before_action(self, action: str, *args: Any, **kwargs: Any) -> bool: - to_return = True - self.next_direction = None - light_state: str - if action in ("click", "hold"): - if len(args) == 2: - attribute, direction = args - elif "attribute" in kwargs and "direction" in kwargs: - attribute, direction = kwargs["attribute"], kwargs["direction"] - else: - raise ValueError( - f"`attribute` and `direction` are mandatory fields for `{action}` action" - ) - light_state = await self.get_entity_state() - self.smooth_power_on_check = self.check_smooth_power_on( - attribute, direction, light_state - ) - self.remove_transition_check = await self.check_remove_transition( - on_from_user=False - ) - to_return = (light_state == "on") or self.smooth_power_on_check - elif action == "attribute_from_controller_step": - light_state = await self.get_entity_state() - to_return = light_state == "on" - self.smooth_power_on_check = False - self.remove_transition_check = False - else: - self.remove_transition_check = await self.check_remove_transition( - on_from_user=True - ) - self.smooth_power_on_check = False - return await super().before_action(action, *args, **kwargs) and to_return - - @action - async def click( - self, - attribute: str, - direction: str, - mode: str = StepperMode.STOP, - steps: Number | None = None, - ) -> None: - attribute = self.get_option( - attribute, LightController.ATTRIBUTES_LIST, "`click` action" - ) - direction = self.get_option( - direction, [StepperDir.UP, StepperDir.DOWN], "`click` action" - ) - mode = self.get_option( - mode, [StepperMode.STOP, StepperMode.LOOP], "`click` action" - ) - attribute = await self.get_attribute(attribute) - self.value_attribute = await self.get_value_attribute(attribute) - await self.change_light_state( - self.value_attribute, - attribute, - direction, - self.get_stepper(attribute, steps or self.manual_steps, mode, tag="click"), - ) - - @action - async def hold( # type: ignore[override] - self, - attribute: str, - direction: str, - mode: str = StepperMode.STOP, - steps: Number | None = None, - ) -> None: - await self._hold(attribute, direction, mode, steps) - - async def _hold( - self, - attribute: str, - direction: str, - mode: str = StepperMode.STOP, - steps: Number | None = None, - ) -> None: - attribute = self.get_option( - attribute, LightController.ATTRIBUTES_LIST, "`hold` action" - ) - direction = self.get_option( - direction, - [StepperDir.UP, StepperDir.DOWN, StepperDir.TOGGLE], - "`hold` action", - ) - mode = self.get_option( - mode, - [StepperMode.STOP, StepperMode.LOOP, StepperMode.BOUNCE], - "`hold` action", - ) - attribute = await self.get_attribute(attribute) - self.value_attribute = await self.get_value_attribute(attribute) - self.log( - f"Attribute value before running the hold action: {self.value_attribute}", - level="DEBUG", - ) - stepper = self.get_stepper( - attribute, steps or self.automatic_steps, mode, tag="hold" - ) - if direction == StepperDir.TOGGLE: - self.log( - f"Previous direction: {stepper.previous_direction}", - level="DEBUG", - ) - direction = stepper.get_direction(self.value_attribute, direction) - self.log(f"Going direction: {direction}", level="DEBUG") - await super().hold(attribute, direction, stepper) - - async def hold_loop( - self, - attribute: str, - direction: str, - stepper: Stepper, - ) -> bool: - if self.value_attribute is None: - return True - extra_attributes = {"transition": self.delay / 1000} - return await self.change_light_state( - self.value_attribute, - attribute, - direction, - stepper, - extra_attributes=extra_attributes, - ) - - async def change_light_state( - self, - old: float, - attribute: str, - direction: str, - stepper: Stepper, - *, - extra_attributes: dict[str, Any] | None = None, - ) -> bool: - """ - This functions changes the state of the light depending on the previous - value and attribute. It returns True when no more changes will need to be done. - Otherwise, it returns False. - """ - attributes: dict[str, Any] = ( - {} if extra_attributes is None else extra_attributes - ) - direction = self.next_direction or direction - if attribute == LightController.ATTRIBUTE_XY_COLOR: - stepper_output = stepper.step(self.index_color, direction) - self.index_color = int(stepper_output.next_value) - xy_color = self.color_wheel[self.index_color] - attributes[attribute] = list(xy_color) - await self._on(**attributes) - # In case of xy_color mode it never finishes the loop, the hold loop - # will only stop if the hold action is called when releasing the button. - # I haven't experimented any problems with it, but a future implementation - # would be to force the loop to stop after 4 or 5 loops as a safety measure. - return False - if self.smooth_power_on_check: - await self._on_min(attribute) - # # After smooth power on, the light should not brighten up. - return True - stepper_output = stepper.step(old, direction) - self.next_direction = stepper_output.next_direction - next_value = int(stepper_output.next_value) - attributes[attribute] = next_value - await self._on(**attributes) - self.value_attribute = next_value - return stepper_output.exceeded - - def supports_smooth_power_on(self) -> bool: - """ - This function can be overrided for each device to indicate the default behaviour of the controller - when the associated light is off and an event for incrementing brightness is received. - Returns True if the associated light should be turned on with minimum brightness if an event for incrementing - brightness is received, while the lamp is off. - The behaviour can be overridden by the user with the 'smooth_power_on' option in app configuration. - """ - return False diff --git a/appdaemon/apps/controllerx/cx_core/type/media_player_controller.py b/appdaemon/apps/controllerx/cx_core/type/media_player_controller.py deleted file mode 100644 index b03dad2..0000000 --- a/appdaemon/apps/controllerx/cx_core/type/media_player_controller.py +++ /dev/null @@ -1,196 +0,0 @@ -from typing import Any - -from cx_const import MediaPlayer, Number, PredefinedActionsMapping, StepperDir -from cx_core.controller import action -from cx_core.feature_support.media_player import MediaPlayerSupport -from cx_core.integration import EventData -from cx_core.integration.z2m import Z2MIntegration -from cx_core.release_hold_controller import ReleaseHoldController -from cx_core.stepper import MinMax -from cx_core.stepper.index_loop_stepper import IndexLoopStepper -from cx_core.stepper.stop_stepper import StopStepper -from cx_core.type_controller import Entity, TypeController - -DEFAULT_VOLUME_STEPS = 10 - - -class MediaPlayerController(TypeController[Entity], ReleaseHoldController): - domains = ["media_player"] - entity_arg = "media_player" - volume_level: float - - async def init(self) -> None: - volume_steps = self.args.get("volume_steps", DEFAULT_VOLUME_STEPS) - self.volume_stepper = StopStepper(MinMax(0, 1), volume_steps) - self.volume_level = 0.0 - await super().init() - - def _get_entity_type(self) -> type[Entity]: - return Entity - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - return { - MediaPlayer.HOLD_VOLUME_DOWN: (self.hold, (StepperDir.DOWN,)), - MediaPlayer.HOLD_VOLUME_UP: (self.hold, (StepperDir.UP,)), - MediaPlayer.CLICK_VOLUME_DOWN: self.volume_down, - MediaPlayer.CLICK_VOLUME_UP: self.volume_up, - MediaPlayer.VOLUME_SET: self.volume_set, - MediaPlayer.RELEASE: self.release, - MediaPlayer.PLAY: self.play, - MediaPlayer.PAUSE: self.pause, - MediaPlayer.PLAY_PAUSE: self.play_pause, - MediaPlayer.NEXT_TRACK: self.next_track, - MediaPlayer.PREVIOUS_TRACK: self.previous_track, - MediaPlayer.NEXT_SOURCE: (self.change_source_list, (StepperDir.UP,)), - MediaPlayer.PREVIOUS_SOURCE: (self.change_source_list, (StepperDir.DOWN,)), - MediaPlayer.MUTE: self.volume_mute, - MediaPlayer.TTS: self.tts, - MediaPlayer.VOLUME_FROM_CONTROLLER_ANGLE: self.volume_from_controller_angle, - } - - @action - async def change_source_list(self, direction: str) -> None: - entity_states = await self.get_entity_state(attribute="all") - entity_attributes = entity_states["attributes"] - source_list: list[str] | None = entity_attributes.get("source_list") - if source_list is None or len(source_list) == 0: - self.log( - f"⚠️ There is no `source_list` parameter in `{self.entity}`", - level="WARNING", - ascii_encode=False, - ) - return - source = entity_attributes.get("source") - new_index_source: Number - if source is None: - new_index_source = 0 - else: - index_source = source_list.index(source) - source_stepper = IndexLoopStepper(len(source_list)) - stepper_output = source_stepper.step(index_source, direction) - new_index_source = stepper_output.next_value - await self.call_service( - "media_player/select_source", - entity_id=self.entity.name, - source=source_list[int(new_index_source)], - ) - - @action - async def play(self) -> None: - await self.call_service("media_player/media_play", entity_id=self.entity.name) - - @action - async def pause(self) -> None: - await self.call_service("media_player/media_pause", entity_id=self.entity.name) - - @action - async def play_pause(self) -> None: - await self.call_service( - "media_player/media_play_pause", entity_id=self.entity.name - ) - - @action - async def previous_track(self) -> None: - await self.call_service( - "media_player/media_previous_track", entity_id=self.entity.name - ) - - @action - async def next_track(self) -> None: - await self.call_service( - "media_player/media_next_track", entity_id=self.entity.name - ) - - @action - async def volume_set(self, volume_level: float) -> None: - await self.call_service( - "media_player/volume_set", - entity_id=self.entity.name, - volume_level=volume_level, - ) - - @action - async def volume_up(self) -> None: - await self.prepare_volume_change() - await self.volume_change(StepperDir.UP) - - @action - async def volume_down(self) -> None: - await self.prepare_volume_change() - await self.volume_change(StepperDir.DOWN) - - @action - async def volume_mute(self) -> None: - await self.call_service("media_player/volume_mute", entity_id=self.entity.name) - - @action - async def tts( - self, - message: str, - service: str = "google_translate_say", - cache: bool | None = None, - language: str | None = None, - options: dict[str, Any] | None = None, - ) -> None: - args: dict[str, Any] = {"entity_id": self.entity.name, "message": message} - if cache is not None: - args["cache"] = cache - if language is not None: - args["language"] = language - if options is not None: - args["options"] = options - await self.call_service(f"tts.{service}", **args) - - @action - async def volume_from_controller_angle( - self, extra: EventData | None = None - ) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_rotation_angle" not in extra: - self.log( - "`action_rotation_angle` is not present in the MQTT payload", - level="WARNING", - ) - return - angle = extra["action_rotation_angle"] - direction = StepperDir.UP if angle > 0 else StepperDir.DOWN - await self._hold(direction) - - @action - async def hold(self, direction: str) -> None: # type: ignore[override] - await self._hold(direction) - - async def _hold(self, direction: str) -> None: - await self.prepare_volume_change() - await super().hold(direction) - - async def prepare_volume_change(self) -> None: - volume_level = await self.get_entity_state(attribute="volume_level") - if volume_level is not None: - self.volume_level = volume_level - - async def volume_change(self, direction: str) -> bool: - if await self.feature_support.is_supported(MediaPlayerSupport.VOLUME_SET): - stepper_output = self.volume_stepper.step(self.volume_level, direction) - self.volume_level = stepper_output.next_value - await self.volume_set(self.volume_level) - return stepper_output.exceeded - else: - if direction == StepperDir.UP: - await self.call_service( - "media_player/volume_up", entity_id=self.entity.name - ) - else: - await self.call_service( - "media_player/volume_down", entity_id=self.entity.name - ) - return False - - async def hold_loop(self, direction: str) -> bool: - return await self.volume_change(direction) - - def default_delay(self) -> int: - return 500 diff --git a/appdaemon/apps/controllerx/cx_core/type/switch_controller.py b/appdaemon/apps/controllerx/cx_core/type/switch_controller.py deleted file mode 100644 index d4cab4a..0000000 --- a/appdaemon/apps/controllerx/cx_core/type/switch_controller.py +++ /dev/null @@ -1,48 +0,0 @@ -from cx_const import PredefinedActionsMapping, Switch -from cx_core.controller import action -from cx_core.type_controller import Entity, TypeController - - -class SwitchController(TypeController[Entity]): - """ - This is the main class that controls the switches for different devices. - Type of actions: - - On/Off/Toggle - Parameters taken: - - controller (required): Inherited from Controller - - switch (required): Switch entity name - """ - - domains = [ - "switch", - "alert", - "automation", - "cover", - "input_boolean", - "light", - "media_player", - "script", - ] - entity_arg = "switch" - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - return { - Switch.ON: self.on, - Switch.OFF: self.off, - Switch.TOGGLE: self.toggle, - } - - def _get_entity_type(self) -> type[Entity]: - return Entity - - @action - async def on(self) -> None: - await self.call_service("homeassistant/turn_on", entity_id=self.entity.name) - - @action - async def off(self) -> None: - await self.call_service("homeassistant/turn_off", entity_id=self.entity.name) - - @action - async def toggle(self) -> None: - await self.call_service("homeassistant/toggle", entity_id=self.entity.name) diff --git a/appdaemon/apps/controllerx/cx_core/type/z2m_light_controller.py b/appdaemon/apps/controllerx/cx_core/type/z2m_light_controller.py deleted file mode 100644 index f6576d3..0000000 --- a/appdaemon/apps/controllerx/cx_core/type/z2m_light_controller.py +++ /dev/null @@ -1,429 +0,0 @@ -import json -from collections.abc import Awaitable, Callable -from functools import lru_cache -from typing import Annotated, Any, Literal - -from cx_const import PredefinedActionsMapping, StepperDir, Z2MLight -from cx_core.controller import Controller, action -from cx_core.integration import EventData -from cx_core.integration.z2m import Z2MIntegration -from cx_core.stepper import InvertStepper, MinMax -from cx_core.type_controller import Entity, TypeController - -DEFAULT_CLICK_STEPS = 70 -DEFAULT_HOLD_STEPS = 70 -DEFAULT_TRANSITION = 0.5 -DEFAULT_MODE = "ha" -DEFAULT_TOPIC_PREFIX = "zigbee2mqtt" - -Mode = Annotated[str, Literal["ha", "mqtt"]] - - -class Z2MLightEntity(Entity): - mode: Mode - topic_prefix: str - - def __init__( - self, - name: str, - entities: list[str] | None = None, - mode: Mode = DEFAULT_MODE, - topic_prefix: str = DEFAULT_TOPIC_PREFIX, - ) -> None: - super().__init__(name, entities) - mode = Controller.get_option(mode, ["ha", "mqtt"]) - self.mode = mode - self.topic_prefix = topic_prefix - - -class Z2MLightController(TypeController[Z2MLightEntity]): - """ - This is the main class that controls the Zigbee2MQTT lights for different devices. - Type of actions: - - On/Off/Toggle - - Brightness click and hold - - Color temp click and hold - """ - - ATTRIBUTE_BRIGHTNESS = "brightness" - ATTRIBUTE_COLOR_TEMP = "color_temp" - - ATTRIBUTES_LIST = [ - ATTRIBUTE_BRIGHTNESS, - ATTRIBUTE_COLOR_TEMP, - ] - - MIN_MAX_ATTR = { - ATTRIBUTE_BRIGHTNESS: MinMax(min=1, max=254), - ATTRIBUTE_COLOR_TEMP: MinMax(min=250, max=454), - } - - entity_arg = "light" - - click_steps: float - hold_steps: float - transition: float - use_onoff: bool - - hold_attribute: str | None - - _mqtt_fn: dict[Mode, Callable[[str, str], Awaitable[None]]] - - async def init(self) -> None: - self.click_steps = self.args.get("click_steps", DEFAULT_CLICK_STEPS) - self.hold_steps = self.args.get("hold_steps", DEFAULT_HOLD_STEPS) - self.transition = self.args.get("transition", DEFAULT_TRANSITION) - self.use_onoff = self.args.get("use_onoff", False) - - self._mqtt_fn = { - "ha": self._ha_mqtt_call, - "mqtt": self._mqtt_plugin_call, - } - self.hold_attribute = None - - await super().init() - - def _get_entity_type(self) -> type[Z2MLightEntity]: - return Z2MLightEntity - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - return { - Z2MLight.ON: self.on, - Z2MLight.OFF: self.off, - Z2MLight.TOGGLE: self.toggle, - Z2MLight.RELEASE: self.release, - Z2MLight.ON_FULL_BRIGHTNESS: ( - self.on_full, - (Z2MLightController.ATTRIBUTE_BRIGHTNESS,), - ), - Z2MLight.ON_FULL_COLOR_TEMP: ( - self.on_full, - (Z2MLightController.ATTRIBUTE_COLOR_TEMP,), - ), - Z2MLight.ON_MIN_BRIGHTNESS: ( - self.on_min, - (Z2MLightController.ATTRIBUTE_BRIGHTNESS,), - ), - Z2MLight.ON_MIN_COLOR_TEMP: ( - self.on_min, - (Z2MLightController.ATTRIBUTE_COLOR_TEMP,), - ), - Z2MLight.SET_HALF_BRIGHTNESS: ( - self.set_value, - ( - Z2MLightController.ATTRIBUTE_BRIGHTNESS, - 0.5, - ), - ), - Z2MLight.SET_HALF_COLOR_TEMP: ( - self.set_value, - ( - Z2MLightController.ATTRIBUTE_COLOR_TEMP, - 0.5, - ), - ), - Z2MLight.CLICK: self.click, - Z2MLight.CLICK_BRIGHTNESS_UP: ( - self.click, - ( - Z2MLightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.UP, - ), - ), - Z2MLight.CLICK_COLOR_TEMP_UP: ( - self.click, - ( - Z2MLightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.UP, - ), - ), - Z2MLight.CLICK_BRIGHTNESS_DOWN: ( - self.click, - ( - Z2MLightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.DOWN, - ), - ), - Z2MLight.CLICK_COLOR_TEMP_DOWN: ( - self.click, - ( - Z2MLightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.DOWN, - ), - ), - Z2MLight.HOLD: self.hold, - Z2MLight.HOLD_BRIGHTNESS_UP: ( - self.hold, - ( - Z2MLightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.UP, - ), - ), - Z2MLight.HOLD_COLOR_TEMP_UP: ( - self.hold, - ( - Z2MLightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.UP, - ), - ), - Z2MLight.HOLD_BRIGHTNESS_DOWN: ( - self.hold, - ( - Z2MLightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.DOWN, - ), - ), - Z2MLight.HOLD_BRIGHTNESS_TOGGLE: ( - self.hold, - ( - Z2MLightController.ATTRIBUTE_BRIGHTNESS, - StepperDir.TOGGLE, - ), - ), - Z2MLight.HOLD_COLOR_TEMP_DOWN: ( - self.hold, - ( - Z2MLightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.DOWN, - ), - ), - Z2MLight.HOLD_COLOR_TEMP_TOGGLE: ( - self.hold, - ( - Z2MLightController.ATTRIBUTE_COLOR_TEMP, - StepperDir.TOGGLE, - ), - ), - Z2MLight.XYCOLOR_FROM_CONTROLLER: self.xycolor_from_controller, - Z2MLight.COLORTEMP_FROM_CONTROLLER: self.colortemp_from_controller, - Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL: self.brightness_from_controller_level, - Z2MLight.BRIGHTNESS_FROM_CONTROLLER_ANGLE: self.brightness_from_controller_angle, - } - - async def before_action(self, action: str, *args: Any, **kwargs: Any) -> bool: - to_return = not (action == "hold" and self.hold_attribute is not None) - return await super().before_action(action, *args, **kwargs) and to_return - - async def _ha_mqtt_call(self, topic: str, payload: str) -> None: - await self.call_service("mqtt.publish", topic=topic, payload=payload) - - async def _mqtt_plugin_call(self, topic: str, payload: str) -> None: - await self.call_service( - "mqtt.publish", topic=topic, payload=payload, namespace="mqtt" - ) - - async def _mqtt_call(self, payload: dict[str, Any]) -> None: - await self._mqtt_fn[self.entity.mode]( - f"{self.entity.topic_prefix}/{self.entity.name}/set", json.dumps(payload) - ) - - async def _on(self, **attributes: Any) -> None: - await self._mqtt_call({"state": "ON", **attributes}) - - @action - async def on(self, attributes: dict[str, float] | None = None) -> None: - attributes = attributes or {} - await self._on(**attributes) - - async def _off(self) -> None: - await self._mqtt_call({"state": "OFF"}) - - @action - async def off(self) -> None: - await self._off() - - async def _toggle(self, **attributes: Any) -> None: - await self._mqtt_call({"state": "TOGGLE", **attributes}) - - @action - async def toggle(self, attributes: dict[str, float] | None = None) -> None: - attributes = attributes or {} - await self._toggle(**attributes) - - async def _set_value(self, attribute: str, fraction: float) -> None: - fraction = max(0, min(fraction, 1)) - min_ = self.MIN_MAX_ATTR[attribute].min - max_ = self.MIN_MAX_ATTR[attribute].max - value = (max_ - min_) * fraction + min_ - await self._on(**{attribute: value}) - - @action - async def set_value(self, attribute: str, fraction: float) -> None: - await self._set_value(attribute, fraction) - - async def _on_full(self, attribute: str) -> None: - await self._set_value(attribute, 1) - - @action - async def on_full(self, attribute: str) -> None: - await self._on_full(attribute) - - async def _on_min(self, attribute: str) -> None: - await self._set_value(attribute, 0) - - @action - async def on_min(self, attribute: str) -> None: - await self._on_min(attribute) - - @lru_cache(maxsize=None) - def get_stepper(self, attribute: str, steps: float, *, tag: str) -> InvertStepper: - previous_direction = StepperDir.DOWN - return InvertStepper(self.MIN_MAX_ATTR[attribute], steps, previous_direction) - - async def _change_light_state( - self, - *, - attribute: str, - direction: str, - stepper: InvertStepper, - transition: float | None, - use_onoff: bool, - mode: str, - ) -> None: - onoff_cmd = ( - "_onoff" if use_onoff and attribute == self.ATTRIBUTE_BRIGHTNESS else "" - ) - stepper_output = stepper.step(stepper.steps, direction) - kwargs = {} - if transition is not None: - kwargs["transition"] = transition - await self._mqtt_call( - { - f"{attribute}_{mode}{onoff_cmd}": stepper_output.next_value, - **kwargs, - } - ) - - @action - async def click( - self, - attribute: str, - direction: str, - steps: float | None = None, - transition: float | None = None, - use_onoff: bool | None = None, - ) -> None: - attribute = self.get_option(attribute, self.ATTRIBUTES_LIST, "`click` action") - direction = self.get_option( - direction, [StepperDir.UP, StepperDir.DOWN], "`click` action" - ) - steps = steps if steps is not None else self.click_steps - stepper = self.get_stepper(attribute, steps, tag="click") - await self._change_light_state( - attribute=attribute, - direction=direction, - stepper=stepper, - transition=transition if transition is not None else self.transition, - use_onoff=use_onoff if use_onoff is not None else self.use_onoff, - mode="step", - ) - - async def _hold( - self, - attribute: str, - direction: str, - steps: float | None = None, - use_onoff: bool | None = None, - ) -> None: - attribute = self.get_option(attribute, self.ATTRIBUTES_LIST, "`hold` action") - direction = self.get_option( - direction, - [StepperDir.UP, StepperDir.DOWN, StepperDir.TOGGLE], - "`hold` action", - ) - steps = steps if steps is not None else self.hold_steps - stepper = self.get_stepper(attribute, steps, tag="hold") - direction = stepper.get_direction(steps, direction) - self.hold_attribute = attribute - await self._change_light_state( - attribute=attribute, - direction=direction, - stepper=stepper, - transition=None, - use_onoff=use_onoff if use_onoff is not None else self.use_onoff, - mode="move", - ) - - @action - async def hold( - self, - attribute: str, - direction: str, - steps: float | None = None, - use_onoff: bool | None = None, - ) -> None: - await self._hold(attribute, direction, steps, use_onoff) - - @action - async def release(self) -> None: - if self.hold_attribute is None: - return - await self._mqtt_call({f"{self.hold_attribute}_move": "stop"}) - self.hold_attribute = None - - @action - async def xycolor_from_controller(self, extra: EventData | None = None) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_color" not in extra: - self.log( - "`action_color` is not present in the MQTT payload", level="WARNING" - ) - return - xy_color = extra["action_color"] - await self._on(color={"x": xy_color["x"], "y": xy_color["y"]}) - - @action - async def colortemp_from_controller(self, extra: EventData | None = None) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_color_temperature" not in extra: - self.log( - "`action_color_temperature` is not present in the MQTT payload", - level="WARNING", - ) - return - await self._on(color_temp=extra["action_color_temperature"]) - - @action - async def brightness_from_controller_level( - self, extra: EventData | None = None - ) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_level" not in extra: - self.log( - "`action_level` is not present in the MQTT payload", - level="WARNING", - ) - return - await self._on(brightness=extra["action_level"]) - - @action - async def brightness_from_controller_angle( - self, - steps: float | None = None, - use_onoff: bool | None = None, - extra: EventData | None = None, - ) -> None: - if extra is None: - self.log("No event data present", level="WARNING") - return - if isinstance(self.integration, Z2MIntegration): - if "action_rotation_angle" not in extra: - self.log( - "`action_rotation_angle` is not present in the MQTT payload", - level="WARNING", - ) - return - angle = extra["action_rotation_angle"] - direction = StepperDir.UP if angle > 0 else StepperDir.DOWN - await self._hold( - self.ATTRIBUTE_BRIGHTNESS, direction, steps=steps, use_onoff=use_onoff - ) diff --git a/appdaemon/apps/controllerx/cx_core/type_controller.py b/appdaemon/apps/controllerx/cx_core/type_controller.py deleted file mode 100644 index 1065686..0000000 --- a/appdaemon/apps/controllerx/cx_core/type_controller.py +++ /dev/null @@ -1,133 +0,0 @@ -import abc -from typing import Any, Generic, TypeVar - -from cx_core.controller import Controller -from cx_core.feature_support import FeatureSupport - -EntityVar = TypeVar("EntityVar", bound="Entity") - - -class Entity: - name: str - entities: list[str] - - def __init__( - self, name: str, entities: list[str] | None = None, **kwargs: Any - ) -> None: - self.name = name - self.set_entities(entities) - - @property - def main(self) -> str: - return self.entities[0] - - @property - def is_group(self) -> bool: - return self.entities[0] != self.name - - def set_entities(self, value: list[str] | None = None) -> None: - self.entities = value if value is not None else [self.name] - - @classmethod - def instantiate( - cls: type[EntityVar], - name: str, - entities: list[str] | None = None, - **params: Any, - ) -> EntityVar: - return cls(name=name, entities=entities, **params) - - def __str__(self) -> str: - return self.name if not self.is_group else f"{self.name}({self.entities})" - - -class TypeController(Controller, abc.ABC, Generic[EntityVar]): - domains: list[str] = [] - entity_arg: str - entity: EntityVar - update_supported_features: bool - feature_support: FeatureSupport - - async def init(self) -> None: - if self.entity_arg not in self.args: - raise ValueError( - f"{self.__class__.__name__} class needs the `{self.entity_arg}` attribute" - ) - self.entity = await self._get_entity(self.args[self.entity_arg]) - self._check_domain(self.entity) - self.update_supported_features = self.args.get( - "update_supported_features", False - ) - supported_features: int | None = self.args.get("supported_features") - self.feature_support = FeatureSupport( - self, supported_features, self.update_supported_features - ) - await super().init() - - @abc.abstractmethod - def _get_entity_type(self) -> type[EntityVar]: - raise NotImplementedError - - async def _get_entities(self, entity_name: str) -> list[str] | None: - entities: str | list[str] | None = await self.get_state( - entity_name, attribute="entity_id" - ) - self.log( - f"Entities from `{entity_name}` (entity_id attribute): `{entities}`", - level="DEBUG", - ) - # If the entity groups other entities, this attribute will be a list - if not isinstance(entities, (list, tuple)): - return None - if entities is not None and len(entities) == 0: - raise ValueError(f"`{entity_name}` does not have any entities registered.") - return entities - - async def _get_entity(self, entity: str | dict[str, Any]) -> EntityVar: - entity_args: dict[str, Any] - entity_name: str - if isinstance(entity, str): - entity_name = entity - entity_args = {} - elif isinstance(entity, dict): - entity_name = entity["name"] - entity_args = {key: value for key, value in entity.items() if key != "name"} - else: - raise ValueError( - f"Type {type(entity)} is not supported for `{self.entity_arg}` attribute" - ) - entities = await self._get_entities(entity_name) if self.domains else None - return self._get_entity_type().instantiate( - name=entity_name, entities=entities, **entity_args - ) - - def _check_domain(self, entity: Entity) -> None: - if not self.domains: - return - if self.contains_templating(entity.name): - return - same_domain = all( - any(elem.startswith(domain + ".") for domain in self.domains) - for elem in entity.entities - ) - if not same_domain: - if entity.is_group: - error_msg = ( - f"All the subentities from {entity} must be from one " - f"of the following domains {self.domains} (e.g. {self.domains[0]}.bedroom)" - ) - else: - error_msg = ( - f"'{entity}' must be from one " - f"of the following domains {self.domains} (e.g. {self.domains[0]}.bedroom)" - ) - raise ValueError(error_msg) - - async def get_entity_state(self, attribute: str | None = None) -> Any: - entity = self.entity.main - if self.update_supported_features: - entities = await self._get_entities(self.entity.name) - self.entity.set_entities(entities) - entity = self.entity.entities[0] - out = await self.get_state(entity, attribute=attribute) - return out diff --git a/appdaemon/apps/controllerx/cx_devices/__init__.py b/appdaemon/apps/controllerx/cx_devices/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/appdaemon/apps/controllerx/cx_devices/adeo.py b/appdaemon/apps/controllerx/cx_devices/adeo.py deleted file mode 100644 index 427a71f..0000000 --- a/appdaemon/apps/controllerx/cx_devices/adeo.py +++ /dev/null @@ -1,46 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class AdeoHRC99CZC045LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "brightness_step_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_step_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - "color_hue_step_up": Light.HOLD_COLOR_UP, - "color_hue_step_down": Light.HOLD_COLOR_DOWN, - "color_saturation_step_up": Light.HOLD_COLOR_UP, - "color_saturation_step_down": Light.HOLD_COLOR_DOWN, - "color_temperature_step_up": Light.HOLD_COLOR_TEMP_UP, - "color_temperature_step_down": Light.HOLD_COLOR_TEMP_DOWN, - "color_stop": Light.RELEASE, - "scene_1": None, - "scene_2": None, - "scene_3": None, - "scene_4": None, - } - - -class AdeoHRC99CZC045Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "off": Z2MLight.OFF, - "brightness_step_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_step_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - "color_hue_step_up": Z2MLight.HOLD_COLOR_TEMP_UP, - "color_hue_step_down": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "color_saturation_step_up": Z2MLight.HOLD_COLOR_TEMP_UP, - "color_saturation_step_down": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "color_temperature_step_up": Z2MLight.HOLD_COLOR_TEMP_UP, - "color_temperature_step_down": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "color_stop": Z2MLight.RELEASE, - "scene_1": None, - "scene_2": None, - "scene_3": None, - "scene_4": None, - } diff --git a/appdaemon/apps/controllerx/cx_devices/aqara.py b/appdaemon/apps/controllerx/cx_devices/aqara.py deleted file mode 100644 index b7a91ff..0000000 --- a/appdaemon/apps/controllerx/cx_devices/aqara.py +++ /dev/null @@ -1,878 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, MediaPlayer, Switch, Z2MLight -from cx_core import ( - LightController, - MediaPlayerController, - SwitchController, - Z2MLightController, -) -from cx_core.integration import EventData - - -class WXKG02LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Light.TOGGLE, - "double_both": Light.CLICK_BRIGHTNESS_UP, - "hold_both": Light.CLICK_BRIGHTNESS_DOWN, - "single_left": Light.TOGGLE, - "double_left": Light.CLICK_BRIGHTNESS_UP, - "hold_left": Light.CLICK_BRIGHTNESS_DOWN, - "single_right": Light.TOGGLE, - "double_right": Light.CLICK_BRIGHTNESS_UP, - "hold_right": Light.CLICK_BRIGHTNESS_DOWN, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, # single left - 1001: Light.CLICK_BRIGHTNESS_DOWN, # long left - 1004: Light.CLICK_BRIGHTNESS_UP, # double left - 2002: Light.TOGGLE, # single right - 2001: Light.CLICK_BRIGHTNESS_DOWN, # long right - 2004: Light.CLICK_BRIGHTNESS_UP, # double right - 3002: Light.TOGGLE, # single both - 3001: Light.CLICK_BRIGHTNESS_DOWN, # long both - 3004: Light.CLICK_BRIGHTNESS_UP, # double both - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "both_single": Light.TOGGLE, - "both_double": Light.CLICK_BRIGHTNESS_UP, - "both_long press": Light.CLICK_BRIGHTNESS_DOWN, - "left_single": Light.TOGGLE, - "left_double": Light.CLICK_BRIGHTNESS_UP, - "left_long press": Light.CLICK_BRIGHTNESS_DOWN, - "right_single": Light.TOGGLE, - "right_double": Light.CLICK_BRIGHTNESS_UP, - "right_long press": Light.CLICK_BRIGHTNESS_DOWN, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXKG02LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Z2MLight.TOGGLE, - "double_both": Z2MLight.CLICK_BRIGHTNESS_UP, - "hold_both": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "single_left": Z2MLight.TOGGLE, - "double_left": Z2MLight.CLICK_BRIGHTNESS_UP, - "hold_left": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "single_right": Z2MLight.TOGGLE, - "double_right": Z2MLight.CLICK_BRIGHTNESS_UP, - "hold_right": Z2MLight.CLICK_BRIGHTNESS_DOWN, - } - - -class WXKG02LMSwitchController(SwitchController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Switch.TOGGLE, - "single_left": Switch.TOGGLE, - "single_right": Switch.TOGGLE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Switch.TOGGLE, - 2002: Switch.TOGGLE, - 3002: Switch.TOGGLE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "both_single": Switch.TOGGLE, - "left_single": Switch.TOGGLE, - "right_single": Switch.TOGGLE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXKG01LMLightController(LightController): - """ - Different states reported from the controller: - single, double, triple, quadruple, - many, hold, release - """ - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "triple": Light.ON_MIN_BRIGHTNESS, - "quadruple": Light.SET_HALF_BRIGHTNESS, - "many": None, - "hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, # single - 1004: Light.ON_FULL_BRIGHTNESS, # double - 1005: Light.ON_MIN_BRIGHTNESS, # triple - 1006: Light.SET_HALF_BRIGHTNESS, # quadruple - 1010: None, # many - 1001: Light.HOLD_BRIGHTNESS_TOGGLE, # hold the button - 1003: Light.RELEASE, # release the button - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "triple": Light.ON_MIN_BRIGHTNESS, - "quadruple": Light.SET_HALF_BRIGHTNESS, - } - - def get_zha_action(self, data: EventData) -> str: - args = data["args"] - if "click_type" in args: - return args["click_type"] - return data["command"] - - -class WXKG01LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Z2MLight.TOGGLE, - "double": Z2MLight.ON_FULL_BRIGHTNESS, - "triple": Z2MLight.ON_MIN_BRIGHTNESS, - "quadruple": Z2MLight.SET_HALF_BRIGHTNESS, - "many": None, - "hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "release": Z2MLight.RELEASE, - } - - -class WXKG11LMRemoteLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1004: Light.ON_FULL_BRIGHTNESS, - 1001: Light.HOLD_BRIGHTNESS_TOGGLE, - 1003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "release": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXKG11LMRemoteZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Z2MLight.TOGGLE, - "double": Z2MLight.ON_FULL_BRIGHTNESS, - "hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "release": Z2MLight.RELEASE, - } - - -class WXKG11LMSensorSwitchLightController(LightController): - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1004: Light.ON_FULL_BRIGHTNESS, - 1005: Light.ON_MIN_BRIGHTNESS, - 1006: Light.SET_HALF_BRIGHTNESS, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "triple": Light.ON_MIN_BRIGHTNESS, - "quadruple": Light.SET_HALF_BRIGHTNESS, - } - - def get_zha_action(self, data: EventData) -> str: - mapping = { - 1: "single", - 2: "double", - 3: "triple", - 4: "quadruple", - } - clicks = data["args"]["value"] - return mapping.get(clicks, "") - - -class WXKG12LMLightController(LightController): - """ - Different states reported from the controller: - single, double, shake, hold, release - """ - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "shake": Light.ON_MIN_BRIGHTNESS, - "hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, # button_1_press - 1004: Light.ON_FULL_BRIGHTNESS, # button_1_double_press - 1007: Light.ON_MIN_BRIGHTNESS, # button_1_shake - 1001: Light.HOLD_BRIGHTNESS_TOGGLE, # button_1_hold - 1003: Light.RELEASE, # button_1_release_after_hold - } - - -class WXKG12LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Z2MLight.TOGGLE, - "double": Z2MLight.ON_FULL_BRIGHTNESS, - "shake": Z2MLight.ON_MIN_BRIGHTNESS, - "hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "release": Z2MLight.RELEASE, - } - - -class MFKZQ01LMLightController(LightController): - """ - This controller allows movement actions for Xiaomi Aqara Smart Cube as - shake, wakeup, fall, slide, flip180 or 90 and rotate_left or right. - Then from the apps.yaml the needed actions can be included and create - different instances for different lights. - """ - - # Different states reported from the controller: - # shake, wakeup, fall, tap, slide, flip180 - # flip90, rotate_left and rotate_right - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "shake": Light.ON_MIN_BRIGHTNESS, - "tap": Light.TOGGLE, - "slide": Light.ON_FULL_BRIGHTNESS, - "flip180": Light.CLICK_COLOR_UP, - "flip90": Light.CLICK_COLOR_DOWN, - "rotate_left": Light.CLICK_BRIGHTNESS_DOWN, - "rotate_right": Light.CLICK_BRIGHTNESS_UP, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1: Light.ON_MIN_BRIGHTNESS, - 6: Light.TOGGLE, - 5: Light.ON_FULL_BRIGHTNESS, - 4: Light.CLICK_COLOR_UP, - 3: Light.CLICK_COLOR_DOWN, - 8: Light.CLICK_BRIGHTNESS_DOWN, - 7: Light.CLICK_BRIGHTNESS_UP, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "shake": Light.ON_MIN_BRIGHTNESS, - "knock": Light.TOGGLE, - "slide": Light.ON_FULL_BRIGHTNESS, - "flip180": Light.CLICK_COLOR_UP, - "flip90": Light.CLICK_COLOR_DOWN, - "rotate_left": Light.CLICK_BRIGHTNESS_DOWN, - "rotate_right": Light.CLICK_BRIGHTNESS_UP, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - action = command - args = data.get("args", {}) - if command == "flip": - action = command + str(args["flip_degrees"]) - return action - - -class WXCJKG11LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": Light.OFF, - "button_1_double": Light.ON_MIN_BRIGHTNESS, - "button_1_hold": Light.HOLD_BRIGHTNESS_DOWN, - "button_1_release": Light.RELEASE, - "button_2_single": Light.ON, - "button_2_double": Light.ON_FULL_BRIGHTNESS, - "button_2_hold": Light.HOLD_BRIGHTNESS_UP, - "button_2_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.OFF, - 1001: Light.HOLD_BRIGHTNESS_DOWN, - 1003: Light.RELEASE, - 2002: Light.ON, - 2001: Light.HOLD_BRIGHTNESS_UP, - 2003: Light.RELEASE, - } - - -class WXCJKG11LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": Z2MLight.OFF, - "button_1_double": Z2MLight.ON_MIN_BRIGHTNESS, - "button_1_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "button_1_release": Z2MLight.RELEASE, - "button_2_single": Z2MLight.ON, - "button_2_double": Z2MLight.ON_FULL_BRIGHTNESS, - "button_2_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "button_2_release": Z2MLight.RELEASE, - } - - -class WXCJKG12LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": Light.OFF, - "button_1_double": Light.ON_MIN_COLOR_TEMP, - "button_1_triple": None, - "button_1_hold": Light.HOLD_COLOR_DOWN, - "button_1_release": Light.RELEASE, - "button_2_single": Light.ON, - "button_2_double": Light.ON_FULL_COLOR_TEMP, - "button_2_triple": None, - "button_2_hold": Light.HOLD_COLOR_UP, - "button_2_release": Light.RELEASE, - "button_3_single": Light.CLICK_BRIGHTNESS_DOWN, - "button_3_double": Light.ON_MIN_BRIGHTNESS, - "button_3_triple": None, - "button_3_hold": Light.HOLD_BRIGHTNESS_DOWN, - "button_3_release": Light.RELEASE, - "button_4_single": Light.CLICK_BRIGHTNESS_UP, - "button_4_double": Light.ON_FULL_BRIGHTNESS, - "button_4_triple": None, - "button_4_hold": Light.HOLD_BRIGHTNESS_UP, - "button_4_release": Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_single": Light.OFF, - "1_double": Light.ON_MIN_COLOR_TEMP, - "1_triple": None, - "1_long press": Light.HOLD_COLOR_DOWN, - "1_release": Light.RELEASE, - "2_single": Light.ON, - "2_double": Light.ON_FULL_COLOR_TEMP, - "2_triple": None, - "2_long press": Light.HOLD_COLOR_UP, - "2_release": Light.RELEASE, - "3_single": Light.CLICK_BRIGHTNESS_DOWN, - "3_double": Light.ON_MIN_BRIGHTNESS, - "3_triple": None, - "3_long press": Light.HOLD_BRIGHTNESS_DOWN, - "3_release": Light.RELEASE, - "4_single": Light.CLICK_BRIGHTNESS_UP, - "4_double": Light.ON_FULL_BRIGHTNESS, - "4_triple": None, - "4_long press": Light.HOLD_BRIGHTNESS_UP, - "4_release": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXCJKG12LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": Z2MLight.OFF, - "button_1_double": Z2MLight.ON_MIN_COLOR_TEMP, - "button_1_triple": None, - "button_1_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "button_1_release": Z2MLight.RELEASE, - "button_2_single": Z2MLight.ON, - "button_2_double": Z2MLight.ON_FULL_COLOR_TEMP, - "button_2_triple": None, - "button_2_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "button_2_release": Z2MLight.RELEASE, - "button_3_single": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "button_3_double": Z2MLight.ON_MIN_BRIGHTNESS, - "button_3_triple": None, - "button_3_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "button_3_release": Z2MLight.RELEASE, - "button_4_single": Z2MLight.CLICK_BRIGHTNESS_UP, - "button_4_double": Z2MLight.ON_FULL_BRIGHTNESS, - "button_4_triple": None, - "button_4_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "button_4_release": Z2MLight.RELEASE, - } - - -class WXCJKG13LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": Light.OFF, - "button_1_double": Light.SYNC, - "button_1_triple": None, - "button_1_hold": None, - "button_1_release": None, - "button_2_single": Light.ON, - "button_2_double": Light.SYNC, - "button_2_triple": None, - "button_2_hold": None, - "button_2_release": None, - "button_3_single": Light.CLICK_BRIGHTNESS_DOWN, - "button_3_double": Light.ON_MIN_BRIGHTNESS, - "button_3_triple": None, - "button_3_hold": Light.HOLD_BRIGHTNESS_DOWN, - "button_3_release": Light.RELEASE, - "button_4_single": Light.CLICK_BRIGHTNESS_UP, - "button_4_double": Light.ON_FULL_BRIGHTNESS, - "button_4_triple": None, - "button_4_hold": Light.HOLD_BRIGHTNESS_UP, - "button_4_release": Light.RELEASE, - "button_5_single": Light.CLICK_COLOR_DOWN, - "button_5_double": Light.ON_MIN_COLOR_TEMP, - "button_5_triple": None, - "button_5_hold": Light.HOLD_COLOR_DOWN, - "button_5_release": Light.RELEASE, - "button_6_single": Light.CLICK_COLOR_UP, - "button_6_double": Light.ON_FULL_COLOR_TEMP, - "button_6_triple": None, - "button_6_hold": Light.HOLD_COLOR_UP, - "button_6_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.OFF, - 1004: Light.SYNC, - 1005: None, - 1001: None, - 1003: None, - 2002: Light.ON, - 2004: Light.SYNC, - 2005: None, - 2001: None, - 2003: None, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - 3004: Light.ON_MIN_BRIGHTNESS, - 3005: None, - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 3003: Light.RELEASE, - 4002: Light.CLICK_BRIGHTNESS_UP, - 4004: Light.ON_FULL_BRIGHTNESS, - 4005: None, - 4001: Light.HOLD_BRIGHTNESS_UP, - 4003: Light.RELEASE, - 5002: Light.CLICK_COLOR_DOWN, - 5004: Light.ON_MIN_COLOR_TEMP, - 5005: None, - 5001: Light.HOLD_COLOR_DOWN, - 5003: Light.RELEASE, - 6002: Light.CLICK_COLOR_UP, - 6004: Light.ON_FULL_COLOR_TEMP, - 6005: None, - 6001: Light.HOLD_COLOR_UP, - 6003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_single": Light.OFF, - "1_double": Light.SYNC, - "1_triple": None, - "1_long": None, - "1_release": None, - "2_single": Light.ON, - "2_double": Light.SYNC, - "2_triple": None, - "2_long": None, - "2_release": None, - "3_single": Light.CLICK_BRIGHTNESS_DOWN, - "3_double": Light.ON_MIN_BRIGHTNESS, - "3_triple": None, - "3_long": Light.HOLD_BRIGHTNESS_DOWN, - "3_release": Light.RELEASE, - "4_single": Light.CLICK_BRIGHTNESS_UP, - "4_double": Light.ON_FULL_BRIGHTNESS, - "4_triple": None, - "4_long": Light.HOLD_BRIGHTNESS_UP, - "4_release": Light.RELEASE, - "5_single": Light.CLICK_COLOR_DOWN, - "5_double": Light.ON_MIN_COLOR_TEMP, - "5_triple": None, - "5_long": Light.HOLD_COLOR_DOWN, - "5_release": Light.RELEASE, - "6_single": Light.CLICK_COLOR_UP, - "6_double": Light.ON_FULL_COLOR_TEMP, - "6_triple": None, - "6_long": Light.HOLD_COLOR_UP, - "6_release": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXCJKG13LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": Z2MLight.OFF, - "button_1_double": Z2MLight.ON_FULL_BRIGHTNESS, - "button_1_triple": None, - "button_1_hold": None, - "button_1_release": None, - "button_2_single": Z2MLight.ON, - "button_2_double": Z2MLight.ON_FULL_BRIGHTNESS, - "button_2_triple": None, - "button_2_hold": None, - "button_2_release": None, - "button_3_single": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "button_3_double": Z2MLight.ON_MIN_BRIGHTNESS, - "button_3_triple": None, - "button_3_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "button_3_release": Z2MLight.RELEASE, - "button_4_single": Z2MLight.CLICK_BRIGHTNESS_UP, - "button_4_double": Z2MLight.ON_FULL_BRIGHTNESS, - "button_4_triple": None, - "button_4_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "button_4_release": Z2MLight.RELEASE, - "button_5_single": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "button_5_double": Z2MLight.ON_MIN_COLOR_TEMP, - "button_5_triple": None, - "button_5_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "button_5_release": Z2MLight.RELEASE, - "button_6_single": Z2MLight.CLICK_COLOR_TEMP_UP, - "button_6_double": Z2MLight.ON_FULL_COLOR_TEMP, - "button_6_triple": None, - "button_6_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "button_6_release": Z2MLight.RELEASE, - } - - -class WXCJKG13LMMediaPlayerController(MediaPlayerController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_single": MediaPlayer.CLICK_VOLUME_DOWN, - "button_1_double": MediaPlayer.MUTE, - "button_1_triple": None, - "button_1_hold": MediaPlayer.HOLD_VOLUME_DOWN, - "button_1_release": MediaPlayer.RELEASE, - "button_2_single": MediaPlayer.CLICK_VOLUME_UP, - "button_2_double": MediaPlayer.PLAY_PAUSE, - "button_2_triple": None, - "button_2_hold": MediaPlayer.HOLD_VOLUME_UP, - "button_2_release": MediaPlayer.RELEASE, - "button_3_single": MediaPlayer.PREVIOUS_TRACK, - "button_3_double": None, - "button_3_triple": None, - "button_3_hold": None, - "button_3_release": None, - "button_4_single": MediaPlayer.NEXT_TRACK, - "button_4_double": None, - "button_4_triple": None, - "button_4_hold": None, - "button_4_release": None, - "button_5_single": MediaPlayer.PREVIOUS_SOURCE, - "button_5_double": None, - "button_5_triple": None, - "button_5_hold": None, - "button_5_release": None, - "button_6_single": MediaPlayer.NEXT_SOURCE, - "button_6_double": None, - "button_6_triple": None, - "button_6_hold": None, - "button_6_release": None, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - ####################### - # Button layout - ####################### - # 1000: top-left - # 2000: top-right - # 3000: middle-left - # 4000: middle-right - # 5000: bottom-left - # 6000: bottom-right - ####################### - # Actions - ####################### - # 0001: hold - # 0002: click - # 0003: release - # 0004: double-click - # 0005: triple-click - ####################### - return { - 1002: MediaPlayer.CLICK_VOLUME_DOWN, - 1001: MediaPlayer.HOLD_VOLUME_DOWN, - 1003: MediaPlayer.RELEASE, - 1004: MediaPlayer.MUTE, - 1005: None, - 2002: MediaPlayer.CLICK_VOLUME_UP, - 2001: MediaPlayer.HOLD_VOLUME_UP, - 2003: MediaPlayer.RELEASE, - 2004: MediaPlayer.PLAY_PAUSE, - 2005: None, - 3002: MediaPlayer.PREVIOUS_TRACK, - 3001: None, - 3003: None, - 3004: None, - 3005: None, - 4002: MediaPlayer.NEXT_TRACK, - 4001: None, - 4003: None, - 4004: None, - 4005: None, - 5002: MediaPlayer.PREVIOUS_SOURCE, - 5001: None, - 5003: None, - 5004: None, - 5005: None, - 6002: MediaPlayer.NEXT_SOURCE, - 6001: None, - 6003: None, - 6004: None, - 6005: None, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_single": MediaPlayer.CLICK_VOLUME_DOWN, - "1_double": MediaPlayer.MUTE, - "1_triple": None, - "1_long": MediaPlayer.HOLD_VOLUME_DOWN, - "1_release": MediaPlayer.RELEASE, - "2_single": MediaPlayer.CLICK_VOLUME_UP, - "2_double": MediaPlayer.PLAY_PAUSE, - "2_triple": None, - "2_long": MediaPlayer.HOLD_VOLUME_UP, - "2_release": MediaPlayer.RELEASE, - "3_single": MediaPlayer.PREVIOUS_TRACK, - "3_double": None, - "3_triple": None, - "3_long": None, - "3_release": None, - "4_single": MediaPlayer.NEXT_TRACK, - "4_double": None, - "4_triple": None, - "4_long": None, - "4_release": None, - "5_single": MediaPlayer.PREVIOUS_SOURCE, - "5_double": None, - "5_triple": None, - "5_long": None, - "5_release": Light.RELEASE, - "6_single": MediaPlayer.NEXT_SOURCE, - "6_double": None, - "6_triple": None, - "6_long": None, - "6_release": None, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXKG06LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.CLICK_BRIGHTNESS_UP, - "hold": Light.CLICK_BRIGHTNESS_UP, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1001: Light.CLICK_BRIGHTNESS_DOWN, - 1004: Light.CLICK_BRIGHTNESS_UP, - } - - -class WXKG06LMSwitchController(SwitchController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return {"single": Light.TOGGLE} - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return {1002: Light.TOGGLE} - - -class WXKG07LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Light.TOGGLE, - "double_both": Light.CLICK_BRIGHTNESS_UP, - "hold_both": Light.CLICK_BRIGHTNESS_DOWN, - "single_left": Light.TOGGLE, - "double_left": Light.CLICK_BRIGHTNESS_UP, - "hold_left": Light.CLICK_BRIGHTNESS_DOWN, - "single_right": Light.TOGGLE, - "double_right": Light.CLICK_BRIGHTNESS_UP, - "hold_right": Light.CLICK_BRIGHTNESS_DOWN, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1001: Light.CLICK_BRIGHTNESS_DOWN, # long left - 1002: Light.TOGGLE, # single left - 1004: Light.CLICK_BRIGHTNESS_UP, # double left - 2001: Light.CLICK_BRIGHTNESS_DOWN, # long right - 2002: Light.TOGGLE, # single right - 2004: Light.CLICK_BRIGHTNESS_UP, # double right - 3001: Light.CLICK_BRIGHTNESS_DOWN, # long both - 3002: Light.TOGGLE, # single both - 3004: Light.CLICK_BRIGHTNESS_UP, # double both - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "both_single": Light.TOGGLE, - "both_double": Light.CLICK_BRIGHTNESS_UP, - "both_long press": Light.CLICK_BRIGHTNESS_DOWN, - "left_single": Light.TOGGLE, - "left_double": Light.CLICK_BRIGHTNESS_UP, - "left_long press": Light.CLICK_BRIGHTNESS_DOWN, - "right_single": Light.TOGGLE, - "right_double": Light.CLICK_BRIGHTNESS_UP, - "right_long press": Light.CLICK_BRIGHTNESS_DOWN, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class WXKG07LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Z2MLight.TOGGLE, - "double_both": Z2MLight.CLICK_BRIGHTNESS_UP, - "hold_both": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "single_left": Z2MLight.TOGGLE, - "double_left": Z2MLight.CLICK_BRIGHTNESS_UP, - "hold_left": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "single_right": Z2MLight.TOGGLE, - "double_right": Z2MLight.CLICK_BRIGHTNESS_UP, - "hold_right": Z2MLight.CLICK_BRIGHTNESS_DOWN, - } - - -class WXKG07LMSwitchController(SwitchController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Switch.TOGGLE, - "single_left": Switch.TOGGLE, - "single_right": Switch.TOGGLE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Switch.TOGGLE, - 2002: Switch.TOGGLE, - 3002: Switch.TOGGLE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "both_single": Switch.TOGGLE, - "left_single": Switch.TOGGLE, - "right_single": Switch.TOGGLE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class ZNXNKG02LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "hold": Light.HOLD_COLOR_TOGGLE, - "release": Light.RELEASE, - "start_rotating": Light.BRIGHTNESS_FROM_CONTROLLER_ANGLE, - "stop_rotating": Light.RELEASE, - } - - -class ZNXNKG02LMMediaPlayerController(MediaPlayerController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": MediaPlayer.PLAY_PAUSE, - "double": MediaPlayer.NEXT_TRACK, - "hold": MediaPlayer.PREVIOUS_TRACK, - "release": MediaPlayer.RELEASE, - "start_rotating": MediaPlayer.VOLUME_FROM_CONTROLLER_ANGLE, - "stop_rotating": MediaPlayer.RELEASE, - } - - -class WXKG15LMLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Light.TOGGLE, - "double_both": Light.CLICK_BRIGHTNESS_UP, - "triple_both": Light.CLICK_BRIGHTNESS_DOWN, - "hold_both": Light.ON_FULL_BRIGHTNESS, - "single_left": Light.TOGGLE, - "double_left": Light.CLICK_BRIGHTNESS_UP, - "triple_left": Light.CLICK_BRIGHTNESS_DOWN, - "hold_left": Light.ON_FULL_BRIGHTNESS, - "single_right": Light.TOGGLE, - "double_right": Light.CLICK_BRIGHTNESS_UP, - "triple_right": Light.CLICK_BRIGHTNESS_DOWN, - "hold_right": Light.ON_FULL_BRIGHTNESS, - } - - -class WXKG15LMZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Z2MLight.TOGGLE, - "double_both": Z2MLight.CLICK_BRIGHTNESS_UP, - "triple_both": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "hold_both": Z2MLight.ON_FULL_BRIGHTNESS, - "single_left": Z2MLight.TOGGLE, - "double_left": Z2MLight.CLICK_BRIGHTNESS_UP, - "triple_left": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "hold_left": Z2MLight.ON_FULL_BRIGHTNESS, - "single_right": Z2MLight.TOGGLE, - "double_right": Z2MLight.CLICK_BRIGHTNESS_UP, - "triple_right": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "hold_right": Z2MLight.ON_FULL_BRIGHTNESS, - } - - -class WXKG15LMSwitchController(SwitchController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_both": Switch.TOGGLE, - "single_left": Switch.TOGGLE, - "single_right": Switch.TOGGLE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/aurora.py b/appdaemon/apps/controllerx/cx_devices/aurora.py deleted file mode 100644 index d85523c..0000000 --- a/appdaemon/apps/controllerx/cx_devices/aurora.py +++ /dev/null @@ -1,37 +0,0 @@ -from cx_const import DefaultActionsMapping, Light -from cx_core import LightController -from cx_core.integration import EventData - - -class AUA1ZBR2GWLightController(LightController): - # Different states reported from the controller: - # on, off, brightness_step_up, brightness_step_down, - # color_temperature_step_up, color_temperature_step_down - # There is one copy of these actions per endpoint - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_toggle": Light.TOGGLE, - "1_step_up": Light.CLICK_BRIGHTNESS_UP, - "1_step_down": Light.CLICK_BRIGHTNESS_DOWN, - "1_step_color_temp_up": Light.CLICK_COLOR_TEMP_UP, - "1_step_color_temp_down": Light.CLICK_COLOR_TEMP_DOWN, - "2_toggle": Light.TOGGLE, - "2_step_up": Light.CLICK_BRIGHTNESS_UP, - "2_step_down": Light.CLICK_BRIGHTNESS_DOWN, - "2_step_color_temp_up": Light.CLICK_COLOR_TEMP_UP, - "2_step_color_temp_down": Light.CLICK_COLOR_TEMP_DOWN, - } - - def get_zha_action(self, data: EventData) -> str: - endpoint_id = data.get("endpoint_id", 1) - command: str = data["command"] - action = command - args = data.get("args", []) - if command == "step" or command == "step_color_temp": - args_mapping = {0: "up", 1: "down", 3: "up"} - action = "_".join((action, args_mapping[args[0]])) - if command == "on" or command == "off": - action = "toggle" - action = f"{endpoint_id}_{action}" - return action diff --git a/appdaemon/apps/controllerx/cx_devices/homematic.py b/appdaemon/apps/controllerx/cx_devices/homematic.py deleted file mode 100644 index 61344d3..0000000 --- a/appdaemon/apps/controllerx/cx_devices/homematic.py +++ /dev/null @@ -1,78 +0,0 @@ -from cx_const import DefaultActionsMapping, Light -from cx_core import LightController - - -class HMPB2WM552LightController(LightController): - def get_homematic_actions_mapping(self) -> DefaultActionsMapping: - return { - "PRESS_SHORT_1": Light.OFF, - "PRESS_LONG_1": Light.HOLD_BRIGHTNESS_DOWN, - "PRESS_CONT_1": None, - "PRESS_LONG_RELEASE_1": Light.RELEASE, - "PRESS_SHORT_2": Light.ON, - "PRESS_LONG_2": Light.HOLD_BRIGHTNESS_UP, - "PRESS_CONT_2": None, - "PRESS_LONG_RELEASE_2": Light.RELEASE, - } - - -class HMPBI4FMLightController(LightController): - def get_homematic_actions_mapping(self) -> DefaultActionsMapping: - return { - "PRESS_SHORT_1": Light.OFF, - "PRESS_CONT_1": None, - "PRESS_LONG_1": Light.CLICK_COLOR_DOWN, - "PRESS_SHORT_2": Light.ON, - "PRESS_CONT_2": None, - "PRESS_LONG_2": Light.CLICK_COLOR_UP, - "PRESS_SHORT_3": Light.CLICK_BRIGHTNESS_DOWN, - "PRESS_CONT_3": None, - "PRESS_LONG_3": Light.ON_MIN_BRIGHTNESS, - "PRESS_SHORT_4": Light.CLICK_BRIGHTNESS_UP, - "PRESS_CONT_4": None, - "PRESS_LONG_4": Light.ON_FULL_BRIGHTNESS, - } - - -class HMPB6WM55LightController(LightController): - def get_homematic_actions_mapping(self) -> DefaultActionsMapping: - return { - "PRESS_SHORT_1": Light.OFF, - "PRESS_CONT_1": None, - "PRESS_LONG_1": Light.SYNC, - "PRESS_LONG_RELEASE_1": Light.RELEASE, - "PRESS_SHORT_2": Light.ON, - "PRESS_CONT_2": None, - "PRESS_LONG_2": Light.SYNC, - "PRESS_LONG_RELEASE_2": Light.RELEASE, - "PRESS_SHORT_3": Light.CLICK_BRIGHTNESS_DOWN, - "PRESS_CONT_3": None, - "PRESS_LONG_3": Light.HOLD_BRIGHTNESS_DOWN, - "PRESS_LONG_RELEASE_3": Light.RELEASE, - "PRESS_SHORT_4": Light.CLICK_BRIGHTNESS_UP, - "PRESS_CONT_4": None, - "PRESS_LONG_4": Light.HOLD_BRIGHTNESS_UP, - "PRESS_LONG_RELEASE_4": Light.RELEASE, - "PRESS_SHORT_5": Light.CLICK_COLOR_DOWN, - "PRESS_CONT_5": None, - "PRESS_LONG_5": Light.HOLD_COLOR_DOWN, - "PRESS_LONG_RELEASE_5": Light.RELEASE, - "PRESS_SHORT_6": Light.CLICK_COLOR_UP, - "PRESS_CONT_6": None, - "PRESS_LONG_6": Light.HOLD_COLOR_UP, - "PRESS_LONG_RELEASE_6": Light.RELEASE, - } - - -class HMSenMDIRWM55LightController(LightController): - def get_homematic_actions_mapping(self) -> DefaultActionsMapping: - return { - "PRESS_SHORT_1": Light.OFF, - "PRESS_LONG_1": Light.HOLD_BRIGHTNESS_DOWN, - "PRESS_CONT_1": None, - "PRESS_LONG_RELEASE_1": Light.RELEASE, - "PRESS_SHORT_2": Light.ON, - "PRESS_LONG_2": Light.HOLD_BRIGHTNESS_UP, - "PRESS_CONT_2": None, - "PRESS_LONG_RELEASE_2": Light.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/ikea.py b/appdaemon/apps/controllerx/cx_devices/ikea.py deleted file mode 100644 index efe22e4..0000000 --- a/appdaemon/apps/controllerx/cx_devices/ikea.py +++ /dev/null @@ -1,844 +0,0 @@ -from cx_const import ( - Cover, - DefaultActionsMapping, - Light, - MediaPlayer, - PredefinedActionsMapping, - Switch, - Z2MLight, -) -from cx_core import ( - CoverController, - LightController, - MediaPlayerController, - SwitchController, - Z2MLightController, - action, -) -from cx_core.integration import EventData - - -class E1810Controller(LightController): - # Different states reported from the controller: - # toggle, brightness_up_click, brightness_down_click - # arrow_left_click, arrow_right_click, brightness_up_hold - # brightness_up_release, brightness_down_hold, brightness_down_release, - # arrow_left_hold, arrow_left_release, arrow_right_hold - # arrow_right_release - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": Light.TOGGLE, - "toggle_hold": Light.SYNC, - "brightness_up_click": Light.CLICK_BRIGHTNESS_UP, - "brightness_down_click": Light.CLICK_BRIGHTNESS_DOWN, - "arrow_left_click": Light.CLICK_COLOR_DOWN, - "arrow_right_click": Light.CLICK_COLOR_UP, - "brightness_up_hold": Light.HOLD_BRIGHTNESS_UP, - "brightness_up_release": Light.RELEASE, - "brightness_down_hold": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_down_release": Light.RELEASE, - "arrow_left_hold": Light.HOLD_COLOR_DOWN, - "arrow_left_release": Light.RELEASE, - "arrow_right_hold": Light.HOLD_COLOR_UP, - "arrow_right_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1001: Light.SYNC, - 2002: Light.CLICK_BRIGHTNESS_UP, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - 4002: Light.CLICK_COLOR_DOWN, - 5002: Light.CLICK_COLOR_UP, - 2001: Light.HOLD_BRIGHTNESS_UP, - 2003: Light.RELEASE, - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 3003: Light.RELEASE, - 4001: Light.HOLD_COLOR_DOWN, - 4003: Light.RELEASE, - 5001: Light.HOLD_COLOR_UP, - 5003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": Light.TOGGLE, - "press_2_0_0": Light.SYNC, - "step_with_on_off_0_43_5": Light.CLICK_BRIGHTNESS_UP, - "step_1_43_5_0_0": Light.CLICK_BRIGHTNESS_DOWN, - "press_257_13_0": Light.CLICK_COLOR_DOWN, - "press_256_13_0": Light.CLICK_COLOR_UP, - "move_with_on_off_0_83": Light.HOLD_BRIGHTNESS_UP, - "move_with_on_off_0_84": Light.HOLD_BRIGHTNESS_UP, # ZigBee 3.0 firmware - "move_1_83_0_0": Light.HOLD_BRIGHTNESS_DOWN, - "move_1_84_0_0": Light.HOLD_BRIGHTNESS_DOWN, # ZigBee 3.0 firmware - "hold_3329_0": Light.HOLD_COLOR_DOWN, - "hold_3328_0": Light.HOLD_COLOR_UP, - "stop": Light.RELEASE, - "stop_with_on_off": Light.RELEASE, - "release": Light.RELEASE, - } - - -class E1810Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": Z2MLight.TOGGLE, - "toggle_hold": Z2MLight.ON_FULL_BRIGHTNESS, - "brightness_up_click": Z2MLight.CLICK_BRIGHTNESS_UP, - "brightness_down_click": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "arrow_left_click": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "arrow_right_click": Z2MLight.CLICK_COLOR_TEMP_UP, - "brightness_up_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_up_release": Z2MLight.RELEASE, - "brightness_down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_down_release": Z2MLight.RELEASE, - "arrow_left_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "arrow_left_release": Z2MLight.RELEASE, - "arrow_right_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "arrow_right_release": Z2MLight.RELEASE, - } - - -class E1810MediaPlayerController(MediaPlayerController): - # Different states reported from the controller: - # toggle, brightness_up_click, brightness_down_click - # arrow_left_click, arrow_right_click, brightness_up_hold - # brightness_up_release, brightness_down_hold, brightness_down_release, - # arrow_left_hold, arrow_left_release, arrow_right_hold - # arrow_right_release - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": MediaPlayer.PLAY_PAUSE, - "brightness_up_click": MediaPlayer.CLICK_VOLUME_UP, - "brightness_down_click": MediaPlayer.CLICK_VOLUME_DOWN, - "arrow_left_click": MediaPlayer.PREVIOUS_TRACK, - "arrow_right_click": MediaPlayer.NEXT_TRACK, - "arrow_left_hold": MediaPlayer.PREVIOUS_SOURCE, - "arrow_right_hold": MediaPlayer.NEXT_SOURCE, - "brightness_up_hold": MediaPlayer.HOLD_VOLUME_UP, - "brightness_up_release": MediaPlayer.RELEASE, - "brightness_down_hold": MediaPlayer.HOLD_VOLUME_DOWN, - "brightness_down_release": MediaPlayer.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: MediaPlayer.PLAY_PAUSE, - 2002: MediaPlayer.CLICK_VOLUME_UP, - 3002: MediaPlayer.CLICK_VOLUME_DOWN, - 4002: MediaPlayer.PREVIOUS_TRACK, - 5002: MediaPlayer.NEXT_TRACK, - 2001: MediaPlayer.HOLD_VOLUME_UP, - 2003: MediaPlayer.RELEASE, - 3001: MediaPlayer.HOLD_VOLUME_DOWN, - 3003: MediaPlayer.RELEASE, - 4001: MediaPlayer.PREVIOUS_SOURCE, - 5001: MediaPlayer.NEXT_SOURCE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": MediaPlayer.PLAY_PAUSE, - "step_with_on_off_0_43_5": MediaPlayer.CLICK_VOLUME_UP, - "step_1_43_5_0_0": MediaPlayer.CLICK_VOLUME_DOWN, - "press_257_13_0": MediaPlayer.PREVIOUS_TRACK, - "press_256_13_0": MediaPlayer.NEXT_TRACK, - "move_with_on_off_0_83": MediaPlayer.HOLD_VOLUME_UP, - "move_with_on_off_0_84": MediaPlayer.HOLD_VOLUME_UP, # ZigBee 3.0 firmware - "stop": MediaPlayer.RELEASE, - "stop_with_on_off": MediaPlayer.RELEASE, - "move_1_83_0_0": MediaPlayer.HOLD_VOLUME_DOWN, - "move_1_84_0_0": MediaPlayer.HOLD_VOLUME_DOWN, # ZigBee 3.0 firmware - "hold_3329_0": MediaPlayer.PREVIOUS_SOURCE, - "hold_3328_0": MediaPlayer.NEXT_SOURCE, - "release": MediaPlayer.RELEASE, - } - - -class E1743Controller(LightController): - # Different states reported from the controller: - # on, off, brightness_up, brightness_down, brightness_stop - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, - 2002: Light.OFF, - 1001: Light.HOLD_BRIGHTNESS_UP, - 2001: Light.HOLD_BRIGHTNESS_DOWN, - 1003: Light.RELEASE, - 2003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "move_with_on_off_0_83": Light.HOLD_BRIGHTNESS_UP, - "move_1_83_0_0": Light.HOLD_BRIGHTNESS_DOWN, - "stop": Light.RELEASE, - "stop_with_on_off": Light.RELEASE, - } - - -class E1743Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "off": Z2MLight.OFF, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - } - - -class E1743MediaPlayerController(MediaPlayerController): - # Different states reported from the controller: - # on, off, brightness_up, brightness_down, brightness_stop - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": MediaPlayer.PLAY_PAUSE, - "off": MediaPlayer.NEXT_TRACK, - "brightness_move_up": MediaPlayer.HOLD_VOLUME_UP, - "brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN, - "brightness_stop": MediaPlayer.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: MediaPlayer.PLAY_PAUSE, - 2002: MediaPlayer.NEXT_TRACK, - 1001: MediaPlayer.HOLD_VOLUME_UP, - 2001: MediaPlayer.HOLD_VOLUME_DOWN, - 1003: MediaPlayer.RELEASE, - 2003: MediaPlayer.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": MediaPlayer.PLAY_PAUSE, - "off": MediaPlayer.NEXT_TRACK, - "move_with_on_off_0_83": MediaPlayer.HOLD_VOLUME_UP, - "move_1_83_0_0": MediaPlayer.HOLD_VOLUME_DOWN, - "stop": MediaPlayer.RELEASE, - "stop_with_on_off": MediaPlayer.RELEASE, - } - - -class E1743SwitchController(SwitchController): - # Different states reported from the controller: - # on, off - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return {"on": Switch.ON, "off": Switch.OFF} - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return {1002: Switch.ON, 2002: Switch.OFF} - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return {"on": Switch.ON, "off": Switch.OFF} - - -class E1743CoverController(CoverController): - # Different states reported from the controller: - # on, off - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Cover.TOGGLE_OPEN, - "off": Cover.TOGGLE_CLOSE, - "brightness_move_up": Cover.OPEN, - "brightness_move_down": Cover.CLOSE, - "brightness_stop": Cover.STOP, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Cover.TOGGLE_OPEN, - 2002: Cover.TOGGLE_CLOSE, - 1001: Cover.OPEN, - 2001: Cover.CLOSE, - 1003: Cover.STOP, - 2003: Cover.STOP, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Cover.TOGGLE_OPEN, - "off": Cover.TOGGLE_CLOSE, - "move_with_on_off_0_83": Cover.OPEN, - "move_1_83_0_0": Cover.CLOSE, - "stop": Cover.STOP, - "stop_with_on_off": Cover.STOP, - } - - -class E2201LightController(E1743Controller): - pass - - -class E2201Z2MLightController(E1743Z2MLightController): - pass - - -class E2201MediaPlayerController(E1743MediaPlayerController): - pass - - -class E2201SwitchController(E1743SwitchController): - pass - - -class E2201CoverController(E1743CoverController): - pass - - -class ICTCG1Controller(LightController): - # Different states reported from the controller: - # rotate_left, rotate_left_quick - # rotate_right, rotate_right_quick - # rotate_stop - - @action - async def rotate_left_quick(self) -> None: - await self.release() - await self.off() - - @action - async def rotate_right_quick(self) -> None: - await self.release() - await self.on_full(LightController.ATTRIBUTE_BRIGHTNESS) - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - parent_mapping = super().get_predefined_actions_mapping() - mapping: PredefinedActionsMapping = { - "rotate_left_quick": self.rotate_left_quick, - "rotate_right_quick": self.rotate_right_quick, - } - mapping.update(parent_mapping) - return mapping - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - "brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: "rotate_right_quick", - 2002: Light.CLICK_BRIGHTNESS_UP, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - 4002: "rotate_left_quick", - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "move_1_70": Light.HOLD_BRIGHTNESS_DOWN, - "move_1_195": Light.HOLD_BRIGHTNESS_DOWN, - "move_to_level_with_on_off_0_1": "rotate_left_quick", - "move_with_on_off_0_70": Light.HOLD_BRIGHTNESS_UP, - "move_with_on_off_0_195": Light.ON, - "move_to_level_with_on_off_255_1": "rotate_right_quick", - "stop": Light.RELEASE, - } - - -class ICTCG1Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - "brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - } - - -class ICTCG1MediaPlayerController(MediaPlayerController): - # Different states reported from the controller: - # rotate_left, rotate_left_quick - # rotate_right, rotate_right_quick - # rotate_stop - - @action - async def rotate_left_quick(self) -> None: - await self.release() - await self.pause() - - @action - async def rotate_right_quick(self) -> None: - await self.release() - await self.play() - - def get_predefined_actions_mapping(self) -> PredefinedActionsMapping: - parent_mapping = super().get_predefined_actions_mapping() - mapping: PredefinedActionsMapping = { - "rotate_left_quick": self.rotate_left_quick, - "rotate_right_quick": self.rotate_right_quick, - } - mapping.update(parent_mapping) - return mapping - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_up": MediaPlayer.HOLD_VOLUME_UP, - "brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN, - "brightness_stop": MediaPlayer.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: "rotate_right_quick", - 2002: MediaPlayer.CLICK_VOLUME_UP, - 3002: MediaPlayer.CLICK_VOLUME_DOWN, - 4002: "rotate_left_quick", - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "move_1_70": MediaPlayer.HOLD_VOLUME_DOWN, - "move_1_195": MediaPlayer.HOLD_VOLUME_DOWN, - "move_to_level_with_on_off_0_1": "rotate_left_quick", - "move_with_on_off_0_70": MediaPlayer.HOLD_VOLUME_UP, - "move_with_on_off_0_195": MediaPlayer.HOLD_VOLUME_UP, - "move_to_level_with_on_off_255_1": "rotate_right_quick", - "stop": MediaPlayer.RELEASE, - } - - -class E1744LightController(LightController): - # Different states reported from the controller: - # brightness_move_down, brightness_move_up, brightness_stop, - # toggle, brightness_step_up, brightness_step_down - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_stop": Light.RELEASE, - "toggle": Light.TOGGLE, - "brightness_step_up": Light.ON_FULL_BRIGHTNESS, - "brightness_step_down": Light.ON_MIN_BRIGHTNESS, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 2001: Light.HOLD_BRIGHTNESS_UP, - 2003: Light.RELEASE, - 3003: Light.RELEASE, - 1002: Light.TOGGLE, - 1004: Light.ON_FULL_BRIGHTNESS, - 1005: Light.ON_MIN_BRIGHTNESS, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "move_1_195_0_0": Light.HOLD_BRIGHTNESS_DOWN, - "move_0_195_0_0": Light.HOLD_BRIGHTNESS_UP, - "stop": Light.RELEASE, - "toggle": Light.TOGGLE, - "step_0_1_0_0_0": Light.ON_FULL_BRIGHTNESS, - "step_1_1_0_0_0": Light.ON_MIN_BRIGHTNESS, - } - - def default_delay(self) -> int: - return 500 - - -class E1744Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_stop": Z2MLight.RELEASE, - "toggle": Z2MLight.TOGGLE, - "brightness_step_up": Z2MLight.ON_FULL_BRIGHTNESS, - "brightness_step_down": Z2MLight.ON_MIN_BRIGHTNESS, - } - - -class E1744MediaPlayerController(MediaPlayerController): - # Different states reported from the controller: - # brightness_move_down, brightness_move_up, brightness_stop, - # toggle, brightness_step_up, brightness_step_down - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN, - "brightness_move_up": MediaPlayer.HOLD_VOLUME_UP, - "brightness_stop": MediaPlayer.RELEASE, - "toggle": MediaPlayer.PLAY_PAUSE, - "brightness_step_up": MediaPlayer.NEXT_TRACK, - "brightness_step_down": MediaPlayer.PREVIOUS_TRACK, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 2001: MediaPlayer.HOLD_VOLUME_UP, - 3001: MediaPlayer.HOLD_VOLUME_DOWN, - 2003: MediaPlayer.RELEASE, - 3003: MediaPlayer.RELEASE, - 1002: MediaPlayer.PLAY_PAUSE, - 1004: MediaPlayer.NEXT_TRACK, - 1005: MediaPlayer.PREVIOUS_TRACK, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "move_1_195_0_0": MediaPlayer.HOLD_VOLUME_DOWN, - "move_0_195_0_0": MediaPlayer.HOLD_VOLUME_UP, - "stop": MediaPlayer.RELEASE, - "toggle": MediaPlayer.PLAY_PAUSE, - "step_0_1_0_0_0": MediaPlayer.NEXT_TRACK, - "step_1_1_0_0_0": MediaPlayer.PREVIOUS_TRACK, - } - - def default_delay(self) -> int: - return 500 - - -class E1766LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return {"open": Light.ON, "close": Light.OFF} - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, - 1003: Light.ON_FULL_BRIGHTNESS, - 2002: Light.OFF, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "up_open": Light.ON, - "down_close": Light.OFF, - } - - -class E1766Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return {"open": Z2MLight.ON, "close": Z2MLight.OFF} - - -class E1766SwitchController(SwitchController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return {"open": Switch.ON, "close": Switch.OFF} - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return {1002: Switch.ON, 2002: Switch.OFF} - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return {"up_open": Switch.ON, "down_close": Switch.OFF} - - -class E1766CoverController(CoverController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "open": Cover.TOGGLE_OPEN, - "close": Cover.TOGGLE_CLOSE, - "stop": Cover.STOP, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Cover.TOGGLE_OPEN, - 1003: Cover.STOP, - 2002: Cover.TOGGLE_CLOSE, - 2003: Cover.STOP, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "up_open": Cover.TOGGLE_OPEN, - "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, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1001: Light.HOLD_BRIGHTNESS_TOGGLE, - 1003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.TOGGLE, - "move_with_on_off": Light.HOLD_BRIGHTNESS_TOGGLE, - "stop": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class E1812Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.TOGGLE, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "brightness_stop": Z2MLight.RELEASE, - } - - -class E1812SwitchController(SwitchController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return {"on": Switch.TOGGLE} - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return {1002: Switch.TOGGLE} - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return {"on": Light.TOGGLE} - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class E2002LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "arrow_left_click": Light.CLICK_COLOR_DOWN, - "arrow_right_click": Light.CLICK_COLOR_UP, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_stop": Light.RELEASE, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "arrow_left_hold": Light.HOLD_COLOR_DOWN, - "arrow_left_release": Light.RELEASE, - "arrow_right_hold": Light.HOLD_COLOR_UP, - "arrow_right_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, - 2002: Light.OFF, - 3002: Light.CLICK_COLOR_DOWN, - 4002: Light.CLICK_COLOR_UP, - 1001: Light.HOLD_BRIGHTNESS_UP, - 1003: Light.RELEASE, - 2001: Light.HOLD_BRIGHTNESS_DOWN, - 2003: Light.RELEASE, - 3001: Light.HOLD_COLOR_DOWN, - 3003: Light.RELEASE, - 4001: Light.HOLD_COLOR_UP, - 4003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "press_257_13_0": Light.CLICK_COLOR_DOWN, - "press_256_13_0": Light.CLICK_COLOR_UP, - "move_with_on_off_0_83": Light.HOLD_BRIGHTNESS_UP, - "move_1_83_0_0": Light.HOLD_BRIGHTNESS_DOWN, - "hold_3329_0": Light.HOLD_COLOR_DOWN, - "hold_3328_0": Light.HOLD_COLOR_UP, - "stop": Light.RELEASE, - "stop_with_on_off": Light.RELEASE, - "release": Light.RELEASE, - } - - -class W2049LightController(E2002LightController): - async def initialize(self) -> None: - await super().initialize() - self.log( - "⚠️ `W2049LightController` is deprecated and will be removed. Use `E2002LightController` instead.", - level="WARNING", - ascii_encode=False, - ) - - -class E2002Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "off": Z2MLight.OFF, - "arrow_left_click": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "arrow_right_click": Z2MLight.CLICK_COLOR_TEMP_UP, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_stop": Z2MLight.RELEASE, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "arrow_left_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "arrow_left_release": Z2MLight.RELEASE, - "arrow_right_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "arrow_right_release": Z2MLight.RELEASE, - } - - -class E2002MediaPlayerController(MediaPlayerController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": MediaPlayer.PLAY_PAUSE, - "off": MediaPlayer.CLICK_VOLUME_DOWN, - "arrow_left_click": MediaPlayer.PREVIOUS_TRACK, - "arrow_right_click": MediaPlayer.NEXT_TRACK, - "arrow_left_hold": MediaPlayer.PREVIOUS_SOURCE, - "arrow_left_release": MediaPlayer.RELEASE, - "arrow_right_hold": MediaPlayer.NEXT_SOURCE, - "arrow_right_release": MediaPlayer.RELEASE, - "brightness_move_up": MediaPlayer.HOLD_VOLUME_UP, - "brightness_move_down": MediaPlayer.HOLD_VOLUME_DOWN, - "brightness_stop": MediaPlayer.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: MediaPlayer.PLAY_PAUSE, - 2002: MediaPlayer.CLICK_VOLUME_DOWN, - 3002: MediaPlayer.PREVIOUS_TRACK, - 4002: MediaPlayer.NEXT_TRACK, - 1001: MediaPlayer.HOLD_VOLUME_UP, - 1003: MediaPlayer.RELEASE, - 2001: MediaPlayer.HOLD_VOLUME_DOWN, - 2003: MediaPlayer.RELEASE, - 3001: MediaPlayer.PREVIOUS_SOURCE, - 3003: MediaPlayer.RELEASE, - 4001: MediaPlayer.NEXT_SOURCE, - 4003: MediaPlayer.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": MediaPlayer.PLAY_PAUSE, - "off": MediaPlayer.CLICK_VOLUME_DOWN, - "press_257_13_0": MediaPlayer.PREVIOUS_TRACK, - "press_256_13_0": MediaPlayer.NEXT_TRACK, - "move_with_on_off_0_83": MediaPlayer.HOLD_VOLUME_UP, - "move_1_83_0_0": MediaPlayer.HOLD_VOLUME_DOWN, - "hold_3329_0": MediaPlayer.PREVIOUS_SOURCE, - "hold_3328_0": MediaPlayer.NEXT_SOURCE, - "stop": MediaPlayer.RELEASE, - "stop_with_on_off": MediaPlayer.RELEASE, - "release": MediaPlayer.RELEASE, - } - - -class W2049MediaPlayerController(E2002MediaPlayerController): - async def initialize(self) -> None: - await super().initialize() - self.log( - "⚠️ `W2049MediaPlayerController` is deprecated and will be removed. " - "Use `E2002MediaPlayerController` instead.", - level="WARNING", - ascii_encode=False, - ) - - -class E2213LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_long_press": Light.HOLD_BRIGHTNESS_UP, - "2_long_press": Light.HOLD_BRIGHTNESS_DOWN, - "1_short_release": Light.ON, - "2_short_release": Light.OFF, - "1_long_release": Light.RELEASE, - "2_long_release": Light.RELEASE, - "1_double_press": Light.HOLD_COLOR_UP, - "2_double_press": Light.HOLD_COLOR_DOWN, - } - - -class E2213Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_long_press": Z2MLight.HOLD_BRIGHTNESS_UP, - "2_long_press": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "1_short_release": Z2MLight.ON, - "2_short_release": Z2MLight.OFF, - "1_long_release": Z2MLight.RELEASE, - "2_long_release": Z2MLight.RELEASE, - "1_double_press": Z2MLight.HOLD_COLOR_TEMP_UP, - "2_double_press": Z2MLight.HOLD_COLOR_TEMP_DOWN, - } - - -class E2123MediaPlayerController(MediaPlayerController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": MediaPlayer.PLAY_PAUSE, # click Play button - "play_pause": MediaPlayer.PLAY_PAUSE, # click Play button - "track_next": MediaPlayer.NEXT_TRACK, # click Next Track - "track_previous": MediaPlayer.PREVIOUS_TRACK, # click Previous Track - "volume_up": MediaPlayer.CLICK_VOLUME_UP, # click + (Volume up) - "volume_up_hold": MediaPlayer.CLICK_VOLUME_UP, # hold + (Volume up) - "volume_down": MediaPlayer.CLICK_VOLUME_DOWN, # click - (Volume down) - "volume_down_hold": MediaPlayer.CLICK_VOLUME_DOWN, # hold - (Volume down) - "dots_1_initial_press": MediaPlayer.PREVIOUS_SOURCE, # click . (Previous Source) - "dots_2_initial_press": MediaPlayer.NEXT_SOURCE, # click .. (Next Source) - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: MediaPlayer.PLAY, # Play; Press/Release or Hold - 2001: MediaPlayer.HOLD_VOLUME_UP, # +; Hold (every 0.2s) - 2002: MediaPlayer.RELEASE, # +; Press/Release - 3001: MediaPlayer.HOLD_VOLUME_DOWN, # +; Hold (every 0.2s) - 3002: MediaPlayer.RELEASE, # -; Press/Release - 4002: MediaPlayer.PREVIOUS_TRACK, # Previous; Press/Release or Hold - 5002: MediaPlayer.NEXT_TRACK, # Next; Press/Release or Hold - 6000: MediaPlayer.PREVIOUS_SOURCE, # .; Initial Press - 7000: MediaPlayer.NEXT_SOURCE, # ..; Initial Press - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": MediaPlayer.PLAY_PAUSE, # click Play button - "play_pause": MediaPlayer.PLAY_PAUSE, # click Play button - "step_0_1_0_0_0": MediaPlayer.NEXT_TRACK, # click Next Track - "step_1_1_0_0_0": MediaPlayer.PREVIOUS_TRACK, # click Previous Track - "move_with_on_off_0_255": MediaPlayer.CLICK_VOLUME_UP, # click + (Volume up) - "move_0_255_0_0": MediaPlayer.HOLD_VOLUME_UP, # hold + (Volume up) - "move_with_on_off_1_255": MediaPlayer.CLICK_VOLUME_DOWN, # click - (Volume down) - "move_1_255_0_0": MediaPlayer.HOLD_VOLUME_DOWN, # hold - (Volume down) - "2_initial_press_0": MediaPlayer.PREVIOUS_SOURCE, # click . (Previous Source) - "3_initial_press_0": MediaPlayer.NEXT_SOURCE, # click .. (Next Source) - } - - def get_zha_action(self, data: EventData) -> str: - command = data["command"] - args = data["args"] - if isinstance(args, dict): - args = args["args"] - args = list(map(str, args)) - action = command - if not (command == "stop" or command == "release"): - if len(args) > 0: - action += "_" + "_".join(args) - endpoint = data["endpoint_id"] - # We only need to add the endpoint ID for the dot buttons, which use 2 and 3 - if endpoint != 1: - return f"{data['endpoint_id']}_{action}" - else: - return action diff --git a/appdaemon/apps/controllerx/cx_devices/legrand.py b/appdaemon/apps/controllerx/cx_devices/legrand.py deleted file mode 100644 index 53ea06f..0000000 --- a/appdaemon/apps/controllerx/cx_devices/legrand.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import Any - -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController -from cx_core.integration import EventData - - -def get_zha_action_LegrandWallController(data: dict[str, Any]) -> str: - endpoint_id = data.get("endpoint_id", 1) - command: str = data["command"] - action = command - args = data.get("args", {}) - args_mapping = {0: "up", 1: "down"} - if command == "move": - action = "_".join((action, args_mapping[args[0]])) - action = "_".join((str(endpoint_id), action)) - return action - - -class Legrand600083LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_on": Light.ON, - "1_off": Light.OFF, - "1_move_up": Light.HOLD_BRIGHTNESS_UP, - "1_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "1_stop": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - return get_zha_action_LegrandWallController(data) - - -class Legrand600083Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "off": Z2MLight.OFF, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - } - - -class Legrand600088LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_left": Light.ON, - "off_left": Light.OFF, - "brightness_move_up_left": Light.HOLD_COLOR_UP, - "brightness_move_down_left": Light.HOLD_COLOR_DOWN, - "brightness_stop_left": Light.RELEASE, - "on_right": Light.ON_FULL_BRIGHTNESS, - "off_right": Light.ON_MIN_BRIGHTNESS, - "brightness_move_up_right": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_right": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_right": Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_on": Light.ON, - "1_off": Light.OFF, - "1_move_up": Light.HOLD_COLOR_UP, - "1_move_down": Light.HOLD_COLOR_DOWN, - "1_stop": Light.RELEASE, - "2_on": Light.ON_FULL_BRIGHTNESS, - "2_off": Light.ON_MIN_BRIGHTNESS, - "2_move_up": Light.HOLD_BRIGHTNESS_UP, - "2_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "2_stop": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - return get_zha_action_LegrandWallController(data) - - -class Legrand600088Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_left": Z2MLight.ON, - "off_left": Z2MLight.OFF, - "brightness_move_up_left": Z2MLight.HOLD_COLOR_TEMP_UP, - "brightness_move_down_left": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "brightness_stop_left": Z2MLight.RELEASE, - "on_right": Z2MLight.ON_FULL_BRIGHTNESS, - "off_right": Z2MLight.ON_MIN_BRIGHTNESS, - "brightness_move_up_right": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_right": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_right": Z2MLight.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/linkind.py b/appdaemon/apps/controllerx/cx_devices/linkind.py deleted file mode 100644 index 4759ecc..0000000 --- a/appdaemon/apps/controllerx/cx_devices/linkind.py +++ /dev/null @@ -1,38 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class ZS23000278LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "brightness_step_up": Light.CLICK_BRIGHTNESS_UP, - "brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN, - "brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - "color_temperature_move": Light.COLORTEMP_FROM_CONTROLLER, - "color_temperature_move_up": Light.CLICK_COLOR_TEMP_UP, - "color_temperature_move_down": Light.CLICK_COLOR_TEMP_DOWN, - "color_move": Light.XYCOLOR_FROM_CONTROLLER, - } - - -class ZS23000278Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "off": Z2MLight.OFF, - "brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP, - "brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - "color_temperature_move": Z2MLight.COLORTEMP_FROM_CONTROLLER, - "color_temperature_move_up": Z2MLight.CLICK_COLOR_TEMP_UP, - "color_temperature_move_down": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "color_move": Z2MLight.XYCOLOR_FROM_CONTROLLER, - } diff --git a/appdaemon/apps/controllerx/cx_devices/livarno.py b/appdaemon/apps/controllerx/cx_devices/livarno.py deleted file mode 100644 index 055dd71..0000000 --- a/appdaemon/apps/controllerx/cx_devices/livarno.py +++ /dev/null @@ -1,47 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class HG06323LightController(LightController): - # Different states reported from the controller: - # on, off, brightness_step_up, brightness_move_up, - # brightness_step_down, brightness_move_down, brightness_stop - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "brightness_step_up": Light.CLICK_BRIGHTNESS_UP, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_stop": Light.RELEASE, - "brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "off": Light.OFF, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "step_0_51_10": Light.CLICK_BRIGHTNESS_UP, - "move_0_51": Light.HOLD_BRIGHTNESS_UP, - "stop": Light.RELEASE, - "step_1_51_10": Light.CLICK_BRIGHTNESS_DOWN, - "move_1_51": Light.HOLD_BRIGHTNESS_DOWN, - "off": Light.OFF, - } - - -class HG06323Z2MLightController(Z2MLightController): - # Different states reported from the controller: - # on, off, brightness_step_up, brightness_move_up, - # brightness_step_down, brightness_move_down, brightness_stop - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_stop": Z2MLight.RELEASE, - "brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "off": Z2MLight.OFF, - } diff --git a/appdaemon/apps/controllerx/cx_devices/lutron.py b/appdaemon/apps/controllerx/cx_devices/lutron.py deleted file mode 100644 index 732b21e..0000000 --- a/appdaemon/apps/controllerx/cx_devices/lutron.py +++ /dev/null @@ -1,167 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, MediaPlayer, Z2MLight -from cx_core import LightController, MediaPlayerController -from cx_core.type.z2m_light_controller import Z2MLightController - - -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_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 2002: Light.CLICK_BRIGHTNESS_UP, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - } - - -class Z31BRLZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - } - - -class LutronPJ22BLightController(LightController): - def get_state_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_state_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_state_actions_mapping(self) -> DefaultActionsMapping: - return { - "1": Light.ON_FULL_BRIGHTNESS, - "8": Light.HOLD_BRIGHTNESS_UP, - "16": Light.HOLD_BRIGHTNESS_DOWN, - "4": Light.OFF, - "0": Light.RELEASE, - } - - -class LutronPJ22BRLMediaPlayerController(MediaPlayerController): - def get_state_actions_mapping(self) -> DefaultActionsMapping: - return { - "1": MediaPlayer.PLAY_PAUSE, - "8": MediaPlayer.HOLD_VOLUME_UP, - "16": MediaPlayer.HOLD_VOLUME_DOWN, - "4": MediaPlayer.NEXT_TRACK, - "0": MediaPlayer.RELEASE, - } - - -class LutronPJ23BRLLightController(LightController): - def get_state_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, - } - - 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 LutronPJ23BRLMediaPlayerController(MediaPlayerController): - def get_state_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, - } - - def get_lutron_caseta_actions_mapping(self) -> DefaultActionsMapping: - return { - "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, - } - - -class LutronPJ24BLightController(LightController): - def get_state_actions_mapping(self) -> DefaultActionsMapping: - return { - "1": Light.ON_FULL_BRIGHTNESS, - "2": Light.HOLD_BRIGHTNESS_UP, - "4": Light.HOLD_BRIGHTNESS_DOWN, - "8": Light.OFF, - "0": Light.RELEASE, - } - - -class LutronPJ24BMediaPlayerController(MediaPlayerController): - def get_state_actions_mapping(self) -> DefaultActionsMapping: - return { - "1": MediaPlayer.PLAY_PAUSE, - "2": MediaPlayer.HOLD_VOLUME_UP, - "4": MediaPlayer.HOLD_VOLUME_DOWN, - "8": MediaPlayer.NEXT_TRACK, - "0": MediaPlayer.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/muller_licht.py b/appdaemon/apps/controllerx/cx_devices/muller_licht.py deleted file mode 100644 index dd97e68..0000000 --- a/appdaemon/apps/controllerx/cx_devices/muller_licht.py +++ /dev/null @@ -1,118 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController -from cx_core.controller import Controller -from cx_core.integration import EventData - - -class MLI404011LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.TOGGLE, - "off": Light.TOGGLE, - "brightness_down_click": Light.CLICK_BRIGHTNESS_DOWN, - "brightness_down_hold": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_down_release": Light.RELEASE, - "brightness_up_click": Light.CLICK_BRIGHTNESS_UP, - "brightness_up_hold": Light.HOLD_BRIGHTNESS_UP, - "brightness_up_release": Light.RELEASE, - "color_wheel": Light.XYCOLOR_FROM_CONTROLLER, # Color ring press - "color_temp": Light.COLORTEMP_FROM_CONTROLLER, # warm or cold - "scene_1": None, # sunset button - "scene_2": None, # party button - "scene_3": None, # reading button - "scene_4": None, # fire button - "scene_5": None, # heart button - "scene_6": None, # night button - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 2001: Light.HOLD_BRIGHTNESS_UP, - 2002: Light.CLICK_BRIGHTNESS_UP, - 2003: Light.RELEASE, - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - 3003: Light.RELEASE, - 4002: Light.CLICK_COLOR_UP, - 5002: Light.CLICK_COLOR_DOWN, - 6002: Light.XYCOLOR_FROM_CONTROLLER, # Color ring press - 7002: None, # reading button - 8002: None, # sunset button - 9002: None, # party button - 10002: None, # night button - 11002: None, # fire button - 12002: None, # heart button - } - - -class MLI404011Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.TOGGLE, - "off": Z2MLight.TOGGLE, - "brightness_down_click": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "brightness_down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_down_release": Z2MLight.RELEASE, - "brightness_up_click": Z2MLight.CLICK_BRIGHTNESS_UP, - "brightness_up_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_up_release": Z2MLight.RELEASE, - "color_wheel": Z2MLight.XYCOLOR_FROM_CONTROLLER, # Color ring press - "color_temp": Z2MLight.COLORTEMP_FROM_CONTROLLER, # warm or cold - "scene_1": None, # sunset button - "scene_2": None, # party button - "scene_3": None, # reading button - "scene_4": None, # fire button - "scene_5": None, # heart button - "scene_6": None, # night button - } - - -class MLI404002Controller(Controller): - def get_zha_action(self, data: EventData) -> str: - command: str = 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, - } - - -class MLI404002Z2MLightController(MLI404002Controller, Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.TOGGLE, - "off": Z2MLight.TOGGLE, - "brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP, - "brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - "recall_1": Z2MLight.ON_FULL_BRIGHTNESS, - } diff --git a/appdaemon/apps/controllerx/cx_devices/namron.py b/appdaemon/apps/controllerx/cx_devices/namron.py deleted file mode 100644 index a8a08bd..0000000 --- a/appdaemon/apps/controllerx/cx_devices/namron.py +++ /dev/null @@ -1,54 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class Namron4512773LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_l1": Light.ON, - "off_l1": Light.OFF, - "brightness_move_up_l1": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l1": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l1": Light.RELEASE, - "on_l2": Light.ON, - "off_l2": Light.OFF, - "brightness_move_up_l2": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l2": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l2": Light.RELEASE, - "on_l3": Light.ON, - "off_l3": Light.OFF, - "brightness_move_up_l3": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l3": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l3": Light.RELEASE, - "on_l4": Light.ON, - "off_l4": Light.OFF, - "brightness_move_up_l4": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l4": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l4": Light.RELEASE, - } - - -class Namron4512773Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_l1": Z2MLight.ON, - "off_l1": Z2MLight.OFF, - "brightness_move_up_l1": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l1": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l1": Z2MLight.RELEASE, - "on_l2": Z2MLight.ON, - "off_l2": Z2MLight.OFF, - "brightness_move_up_l2": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l2": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l2": Z2MLight.RELEASE, - "on_l3": Z2MLight.ON, - "off_l3": Z2MLight.OFF, - "brightness_move_up_l3": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l3": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l3": Z2MLight.RELEASE, - "on_l4": Z2MLight.ON, - "off_l4": Z2MLight.OFF, - "brightness_move_up_l4": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_l4": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_l4": Z2MLight.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/osram.py b/appdaemon/apps/controllerx/cx_devices/osram.py deleted file mode 100644 index 62f7965..0000000 --- a/appdaemon/apps/controllerx/cx_devices/osram.py +++ /dev/null @@ -1,38 +0,0 @@ -from cx_const import DefaultActionsMapping, Light -from cx_core import LightController -from cx_core.integration import EventData - - -class OsramAC025XX00NJLightController(LightController): - # This mapping works for: AC0251100NJ / AC0251400NJ / AC0251600NJ / AC0251700NJ - # (different models are just different colours) - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, # Click Arrow up - 1001: Light.HOLD_BRIGHTNESS_UP, # Hold Arrow Up - 1003: Light.RELEASE, # Release Arrow Up - 2002: Light.OFF, # Click Arrow down - 2001: Light.HOLD_BRIGHTNESS_DOWN, # Hold Arrow down - 2003: Light.RELEASE, # Release Arrow down - 3002: Light.SYNC, # Click Circle button - 3001: Light.HOLD_COLOR_UP, # Hold Circle button - 3003: Light.RELEASE, # Release Circle button - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_on": Light.ON, - "1_move_with_on_off": Light.HOLD_BRIGHTNESS_UP, - "1_stop": Light.RELEASE, - "3_move_to_level_with_on_off": None, - "3_move_to_color_temp": Light.SYNC, - "3_move_to_saturation": Light.HOLD_COLOR_UP, - "3_move_hue": Light.RELEASE, - "2_off": Light.OFF, - "2_move": Light.HOLD_BRIGHTNESS_DOWN, - "2_stop": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - return f"{data['endpoint_id']}_{data['command']}" diff --git a/appdaemon/apps/controllerx/cx_devices/philips.py b/appdaemon/apps/controllerx/cx_devices/philips.py deleted file mode 100644 index 0c52602..0000000 --- a/appdaemon/apps/controllerx/cx_devices/philips.py +++ /dev/null @@ -1,326 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController -from cx_core.integration import EventData - - -class HueDimmerController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_press_release": Light.ON, - "on_hold": Light.HOLD_COLOR_UP, - "on_hold_release": Light.RELEASE, - "up_press_release": Light.CLICK_BRIGHTNESS_UP, - "up_hold": Light.HOLD_BRIGHTNESS_UP, - "up_hold_release": Light.RELEASE, - "down_press_release": Light.CLICK_BRIGHTNESS_DOWN, - "down_hold": Light.HOLD_BRIGHTNESS_DOWN, - "down_hold_release": Light.RELEASE, - "off_press_release": Light.OFF, - "off_hold": Light.HOLD_COLOR_DOWN, - "off_hold_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, - 1001: Light.HOLD_COLOR_UP, - 1003: Light.RELEASE, - 2002: Light.CLICK_BRIGHTNESS_UP, - 2001: Light.HOLD_BRIGHTNESS_UP, - 2003: Light.RELEASE, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 3003: Light.RELEASE, - 4002: Light.OFF, - 4001: Light.HOLD_COLOR_DOWN, - 4003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "off_long_release": Light.RELEASE, - "off_hold": Light.HOLD_COLOR_DOWN, - "off_short_release": Light.OFF, - "down_long_release": Light.RELEASE, - "down_hold": Light.HOLD_BRIGHTNESS_DOWN, - "down_short_release": Light.CLICK_BRIGHTNESS_DOWN, - "up_long_release": Light.RELEASE, - "up_hold": Light.HOLD_BRIGHTNESS_UP, - "up_short_release": Light.CLICK_BRIGHTNESS_UP, - "on_long_release": Light.RELEASE, - "on_hold": Light.HOLD_COLOR_UP, - "on_short_release": Light.ON, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class HueDimmerZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_press_release": Z2MLight.ON, - "on_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "on_hold_release": Z2MLight.RELEASE, - "up_press_release": Z2MLight.CLICK_BRIGHTNESS_UP, - "up_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "up_hold_release": Z2MLight.RELEASE, - "down_press_release": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "down_hold_release": Z2MLight.RELEASE, - "off_press_release": Z2MLight.OFF, - "off_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "off_hold_release": Z2MLight.RELEASE, - } - - -class Philips929002398602LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_press_release": Light.TOGGLE, - "on_hold": Light.TOGGLE, - "on_hold_release": Light.RELEASE, - "up_press_release": Light.CLICK_BRIGHTNESS_UP, - "up_hold": Light.HOLD_BRIGHTNESS_UP, - "up_hold_release": Light.RELEASE, - "down_press_release": Light.CLICK_BRIGHTNESS_DOWN, - "down_hold": Light.HOLD_BRIGHTNESS_DOWN, - "down_hold_release": Light.RELEASE, - "off_press_release": Light.CLICK_COLOR_UP, - "off_hold": Light.HOLD_COLOR_DOWN, - "off_hold_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1001: Light.TOGGLE, - 1003: Light.RELEASE, - 2002: Light.CLICK_BRIGHTNESS_UP, - 2001: Light.HOLD_BRIGHTNESS_UP, - 2003: Light.RELEASE, - 3002: Light.CLICK_BRIGHTNESS_DOWN, - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 3003: Light.RELEASE, - 4002: Light.CLICK_COLOR_UP, - 4001: Light.HOLD_COLOR_DOWN, - 4003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "off_long_release": Light.RELEASE, - "off_hold": Light.HOLD_COLOR_DOWN, - "off_short_release": Light.CLICK_COLOR_UP, - "down_long_release": Light.RELEASE, - "down_hold": Light.HOLD_BRIGHTNESS_DOWN, - "down_short_release": Light.CLICK_BRIGHTNESS_DOWN, - "up_long_release": Light.RELEASE, - "up_hold": Light.HOLD_BRIGHTNESS_UP, - "up_short_release": Light.CLICK_BRIGHTNESS_UP, - "on_long_release": Light.RELEASE, - "on_hold": Light.TOGGLE, - "on_short_release": Light.TOGGLE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class Philips929002398602Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_press_release": Z2MLight.ON, - "on_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "on_hold_release": Z2MLight.RELEASE, - "up_press_release": Z2MLight.CLICK_BRIGHTNESS_UP, - "up_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "up_hold_release": Z2MLight.RELEASE, - "down_press_release": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "down_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "down_hold_release": Z2MLight.RELEASE, - "off_press_release": Z2MLight.OFF, - "off_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "off_hold_release": Z2MLight.RELEASE, - } - - -class PTM215XLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "press_1": Light.ON, - "release_1": Light.RELEASE, - "press_2": Light.OFF, - "release_2": Light.RELEASE, - "press_3": Light.ON_FULL_BRIGHTNESS, - "release_3": Light.RELEASE, - "press_4": Light.ON_MIN_BRIGHTNESS, - "release_4": Light.RELEASE, - "press_1_and_3": Light.ON_FULL_COLOR_TEMP, - "release_1_and_3": Light.RELEASE, - "press_2_and_4": Light.ON_MIN_COLOR_TEMP, - "release_2_and_4": Light.RELEASE, - "press_energy_bar": Light.SYNC, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, # button_1_release - 1001: Light.SYNC, # button_1_hold - 1000: None, # button_1_press - 1003: None, # button_1_release_after_hold - 2002: Light.OFF, # button_2_release - 2001: Light.SYNC, # button_2_hold - 2000: None, # button_2_press - 2003: None, # button_2_release_after_hold - 3002: Light.ON_FULL_BRIGHTNESS, # button_3_release - 3001: Light.HOLD_BRIGHTNESS_UP, # button_3_hold - 3000: None, # button_3_press - 3003: Light.RELEASE, # button_3_release_after_hold - 4002: Light.ON_MIN_BRIGHTNESS, # button_4_release - 4001: Light.HOLD_BRIGHTNESS_DOWN, # button_4_hold - 4000: None, # button_4_press - 4003: Light.RELEASE, # button_4_release_after_hold - 5002: Light.ON_FULL_COLOR_TEMP, # button_1_3_release - 5001: Light.HOLD_COLOR_UP, # button_1_3_hold - 5000: None, # button_1_3_press - 5003: Light.RELEASE, # button_1_3_release_after_hold - 6002: Light.ON_MIN_COLOR_TEMP, # button_2_4_release - 6001: Light.HOLD_COLOR_DOWN, # button_2_4_hold - 6000: None, # button_2_4_press - 6003: Light.RELEASE, # button_2_4_release_after_hold - } - - -class HueSmartButtonLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.TOGGLE, - "off": Light.TOGGLE, - "hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1000: None, # Initial press - 1001: Light.HOLD_BRIGHTNESS_TOGGLE, - 1002: Light.TOGGLE, - 1003: Light.RELEASE, - } - - -class HueSmartButtonZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.TOGGLE, - "off": Z2MLight.TOGGLE, - "hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "release": Z2MLight.RELEASE, - } - - -class Philips929003017102LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - # "left_press": Light.TOGGLE, - "left_press_release": Light.TOGGLE, - "left_hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "left_hold_release": Light.RELEASE, - # "right_press": Light.TOGGLE, - "right_press_release": Light.TOGGLE, - "right_hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "right_hold_release": Light.RELEASE, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1000: None, # Initial press - 1001: Light.HOLD_BRIGHTNESS_TOGGLE, - 1002: Light.TOGGLE, - 1003: Light.RELEASE, - 2000: None, # Initial press - 2001: Light.HOLD_BRIGHTNESS_TOGGLE, - 2002: Light.TOGGLE, - 2003: Light.RELEASE, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - # "left_press": Light.TOGGLE, - "left_press_release": Light.TOGGLE, - "left_hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "left_hold_release": Light.RELEASE, - # "right_press": Light.TOGGLE, - "right_short_release": Light.TOGGLE, - "right_hold": Light.HOLD_BRIGHTNESS_TOGGLE, - "right_long_release": Light.RELEASE, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class Philips929003017102Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - # "left_press": Z2MLight.TOGGLE, - "left_press_release": Z2MLight.TOGGLE, - "left_hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "left_hold_release": Z2MLight.RELEASE, - # "right_press": Z2MLight.TOGGLE, - "right_press_release": Z2MLight.TOGGLE, - "right_hold": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "right_hold_release": Z2MLight.RELEASE, - } - - -class PhilipsRDM002LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_press_release": Light.OFF, - "button_1_hold": Light.HOLD_BRIGHTNESS_DOWN, - "button_1_hold_release": Light.RELEASE, - "button_2_press_release": Light.ON, - "button_2_hold": Light.HOLD_BRIGHTNESS_UP, - "button_2_hold_release": Light.RELEASE, - "button_3_press_release": Light.CLICK_COLOR_DOWN, - "button_3_hold": Light.HOLD_COLOR_DOWN, - "button_3_hold_release": Light.RELEASE, - "button_4_press_release": Light.CLICK_COLOR_UP, - "button_4_hold": Light.HOLD_COLOR_UP, - "button_4_hold_release": Light.RELEASE, - "dial_rotate_left_step": Light.CLICK_BRIGHTNESS_DOWN, - "dial_rotate_left_slow": Light.ON_MIN_COLOR_TEMP, - "dial_rotate_left_fast": Light.ON_MIN_BRIGHTNESS, - "dial_rotate_right_step": Light.CLICK_BRIGHTNESS_UP, - "dial_rotate_right_slow": Light.ON_FULL_COLOR_TEMP, - "dial_rotate_right_fast": Light.ON_FULL_BRIGHTNESS, - } - - -class PhilipsRDM002Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_1_press_release": Z2MLight.OFF, - "button_1_hold": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "button_1_hold_release": Z2MLight.RELEASE, - "button_2_press_release": Z2MLight.ON, - "button_2_hold": Z2MLight.HOLD_BRIGHTNESS_UP, - "button_2_hold_release": Z2MLight.RELEASE, - "button_3_press_release": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "button_3_hold": Z2MLight.HOLD_COLOR_TEMP_DOWN, - "button_3_hold_release": Z2MLight.RELEASE, - "button_4_press_release": Z2MLight.CLICK_COLOR_TEMP_UP, - "button_4_hold": Z2MLight.HOLD_COLOR_TEMP_UP, - "button_4_hold_release": Z2MLight.RELEASE, - "dial_rotate_left_step": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "dial_rotate_left_slow": Z2MLight.ON_MIN_COLOR_TEMP, - "dial_rotate_left_fast": Z2MLight.ON_MIN_BRIGHTNESS, - "dial_rotate_right_step": Light.CLICK_BRIGHTNESS_UP, - "dial_rotate_right_slow": Z2MLight.ON_FULL_COLOR_TEMP, - "dial_rotate_right_fast": Z2MLight.ON_FULL_BRIGHTNESS, - } diff --git a/appdaemon/apps/controllerx/cx_devices/prolight.py b/appdaemon/apps/controllerx/cx_devices/prolight.py deleted file mode 100644 index 299a0a2..0000000 --- a/appdaemon/apps/controllerx/cx_devices/prolight.py +++ /dev/null @@ -1,34 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class Prolight5412748727388LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - "brightness_move_to_level": Light.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - "color_temperature_move": Light.COLORTEMP_FROM_CONTROLLER, - "color_temperature_move_up": Light.CLICK_COLOR_TEMP_UP, - "color_temperature_move_down": Light.CLICK_COLOR_TEMP_DOWN, - "color_move": Light.XYCOLOR_FROM_CONTROLLER, - } - - -class Prolight5412748727388Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "off": Z2MLight.OFF, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - "brightness_move_to_level": Z2MLight.BRIGHTNESS_FROM_CONTROLLER_LEVEL, - "color_temperature_move": Z2MLight.COLORTEMP_FROM_CONTROLLER, - "color_temperature_move_up": Z2MLight.CLICK_COLOR_TEMP_UP, - "color_temperature_move_down": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "color_move": Z2MLight.XYCOLOR_FROM_CONTROLLER, - } diff --git a/appdaemon/apps/controllerx/cx_devices/rgb_genie.py b/appdaemon/apps/controllerx/cx_devices/rgb_genie.py deleted file mode 100644 index d861cb7..0000000 --- a/appdaemon/apps/controllerx/cx_devices/rgb_genie.py +++ /dev/null @@ -1,90 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController -from cx_core.controller import action -from cx_core.integration import EventData -from cx_core.integration.zha import ZHAIntegration - - -class ZB5121LightController(LightController): - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "off": Light.OFF, - "step_with_on_off_0_32_0": Light.CLICK_BRIGHTNESS_UP, # Click brightness up - "move_with_on_off_0_50": Light.HOLD_BRIGHTNESS_UP, # Hold brightness up - "step_with_on_off_1_32_0": Light.CLICK_BRIGHTNESS_DOWN, # Click brightness down - "move_with_on_off_1_50": Light.HOLD_BRIGHTNESS_DOWN, # Hold brightness down - "recall_0_1": None, # Click clapperboard - "stop": Light.RELEASE, # long release - } - - -class ZB5122LightController(LightController): - @action - async def colortemp_from_controller(self, extra: EventData) -> None: - if isinstance(self.integration, ZHAIntegration): - await self._on(color_temp=extra["args"][0]) - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, # Click light on - "off": Light.OFF, # Click light off - "hold_brightness_up": Light.HOLD_BRIGHTNESS_UP, # Hold light on - "hold_brightness_down": Light.HOLD_BRIGHTNESS_DOWN, # Hold light off - "stop": Light.RELEASE, # long release - "move_to_color": Light.CLICK_XY_COLOR_UP, # click RGB - "move_hue": Light.HOLD_XY_COLOR_UP, # hold RGB - "stop_move_hue": Light.RELEASE, # release RGB - "move_to_color_temp": Light.COLORTEMP_FROM_CONTROLLER, # click CW - "move_color_temp": Light.HOLD_COLOR_TEMP_TOGGLE, # hold CW - "stop_move_step": Light.RELEASE, # release CW - "recall_0_1": None, # Click clapperboard - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - if command == "move_with_on_off": - return ( - "hold_brightness_up" if data["args"][0] == 0 else "hold_brightness_down" - ) - elif command == "move_hue": - return "stop_move_hue" if tuple(data["args"]) == (0, 0) else "move_hue" - return command - - -class ZB3009LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.TOGGLE, - "off": Light.TOGGLE, - "brightness_move_up": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Light.RELEASE, - "color_temperature_move_down": Light.CLICK_COLOR_TEMP_DOWN, - "color_temperature_move_up": Light.CLICK_COLOR_TEMP_UP, - "color_temperature_move": Light.COLORTEMP_FROM_CONTROLLER, - "color_move": Light.XYCOLOR_FROM_CONTROLLER, - "hue_move": None, # Play/pause button - "recall_1": None, # Scene 1 - "recall_3": None, # Scene 2 - "recall_2": None, # Scene 3 - } - - -class ZB3009Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.TOGGLE, - "off": Z2MLight.TOGGLE, - "brightness_move_up": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop": Z2MLight.RELEASE, - "color_temperature_move_down": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "color_temperature_move_up": Z2MLight.CLICK_COLOR_TEMP_UP, - "color_temperature_move": Z2MLight.COLORTEMP_FROM_CONTROLLER, - "color_move": Z2MLight.XYCOLOR_FROM_CONTROLLER, - "hue_move": None, # Play/pause button - "recall_1": None, # Scene 1 - "recall_3": None, # Scene 2 - "recall_2": None, # Scene 3 - } diff --git a/appdaemon/apps/controllerx/cx_devices/robb.py b/appdaemon/apps/controllerx/cx_devices/robb.py deleted file mode 100644 index 6bf4e73..0000000 --- a/appdaemon/apps/controllerx/cx_devices/robb.py +++ /dev/null @@ -1,54 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class ROB2000070LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_1": Light.ON, - "off_1": Light.OFF, - "brightness_move_up_1": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_1": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_1": Light.RELEASE, - "on_2": Light.ON, - "off_2": Light.OFF, - "brightness_move_up_2": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_2": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_2": Light.RELEASE, - "on_3": Light.ON, - "off_3": Light.OFF, - "brightness_move_up_3": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_3": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_3": Light.RELEASE, - "on_4": Light.ON, - "off_4": Light.OFF, - "brightness_move_up_4": Light.HOLD_BRIGHTNESS_UP, - "brightness_move_down_4": Light.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_4": Light.RELEASE, - } - - -class ROB2000070Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on_1": Z2MLight.ON, - "off_1": Z2MLight.OFF, - "brightness_move_up_1": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_1": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_1": Z2MLight.RELEASE, - "on_2": Z2MLight.ON, - "off_2": Z2MLight.OFF, - "brightness_move_up_2": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_2": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_2": Z2MLight.RELEASE, - "on_3": Z2MLight.ON, - "off_3": Z2MLight.OFF, - "brightness_move_up_3": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_3": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_3": Z2MLight.RELEASE, - "on_4": Z2MLight.ON, - "off_4": Z2MLight.OFF, - "brightness_move_up_4": Z2MLight.HOLD_BRIGHTNESS_UP, - "brightness_move_down_4": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "brightness_stop_4": Z2MLight.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/sengled.py b/appdaemon/apps/controllerx/cx_devices/sengled.py deleted file mode 100644 index c36fa98..0000000 --- a/appdaemon/apps/controllerx/cx_devices/sengled.py +++ /dev/null @@ -1,48 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, Z2MLight -from cx_core import LightController, Z2MLightController - - -class E1EG7FLightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "on_long": Light.CLICK_COLOR_UP, - "on_double": Light.ON_FULL_COLOR_TEMP, - "up": Light.CLICK_BRIGHTNESS_UP, - "up_long": Light.ON_FULL_BRIGHTNESS, - "down": Light.CLICK_BRIGHTNESS_DOWN, - "down_long": Light.ON_MIN_BRIGHTNESS, - "off": Light.OFF, - "off_long": Light.CLICK_COLOR_DOWN, - "off_double": Light.ON_MIN_COLOR_TEMP, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "on_long": Light.CLICK_COLOR_UP, - "on_double": Light.ON_FULL_COLOR_TEMP, - "step_0_1_0": Light.CLICK_BRIGHTNESS_UP, - "step_0_2_0": Light.ON_FULL_BRIGHTNESS, - "step_1_1_0": Light.CLICK_BRIGHTNESS_DOWN, - "step_1_2_0": Light.ON_MIN_BRIGHTNESS, - "off": Light.OFF, - "off_long": Light.CLICK_COLOR_DOWN, - "off_double": Light.ON_MIN_COLOR_TEMP, - } - - -class E1EG7FZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "on_long": Z2MLight.CLICK_COLOR_TEMP_UP, - "on_double": Z2MLight.ON_FULL_COLOR_TEMP, - "up": Z2MLight.CLICK_BRIGHTNESS_UP, - "up_long": Z2MLight.ON_FULL_BRIGHTNESS, - "down": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "down_long": Z2MLight.ON_MIN_BRIGHTNESS, - "off": Z2MLight.OFF, - "off_long": Z2MLight.CLICK_COLOR_TEMP_DOWN, - "off_double": Z2MLight.ON_MIN_COLOR_TEMP, - } diff --git a/appdaemon/apps/controllerx/cx_devices/shelly.py b/appdaemon/apps/controllerx/cx_devices/shelly.py deleted file mode 100644 index fca81a0..0000000 --- a/appdaemon/apps/controllerx/cx_devices/shelly.py +++ /dev/null @@ -1,57 +0,0 @@ -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, - } - - -class ShellyDimmer2LightController(LightController): - def get_shelly_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_1": Light.TOGGLE, - "single_2": None, - "long_1": Light.TOGGLE, - "long_2": None, - } diff --git a/appdaemon/apps/controllerx/cx_devices/smartkontakten.py b/appdaemon/apps/controllerx/cx_devices/smartkontakten.py deleted file mode 100644 index 255f0f2..0000000 --- a/appdaemon/apps/controllerx/cx_devices/smartkontakten.py +++ /dev/null @@ -1,25 +0,0 @@ -from cx_const import DefaultActionsMapping, Light -from cx_core import LightController - - -class SK5700002228949LightController(LightController): - # This mapping works for: 5700002228949, 5700002228963, 5745000433087 - # The buttons are distributted like: - # 1 3 - # 2 4 - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.ON, - 1001: Light.HOLD_BRIGHTNESS_UP, - 1003: Light.RELEASE, - 3002: Light.OFF, - 3001: Light.HOLD_BRIGHTNESS_DOWN, - 3003: Light.RELEASE, - 2002: Light.CLICK_COLOR_UP, - 2001: Light.HOLD_COLOR_UP, - 2003: Light.RELEASE, - 4002: Light.CLICK_COLOR_DOWN, - 4001: Light.HOLD_COLOR_DOWN, - 4003: Light.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/smartthings.py b/appdaemon/apps/controllerx/cx_devices/smartthings.py deleted file mode 100644 index eaffa85..0000000 --- a/appdaemon/apps/controllerx/cx_devices/smartthings.py +++ /dev/null @@ -1,58 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, MediaPlayer -from cx_core import LightController, MediaPlayerController - - -class SmartThingsButtonLightController(LightController): - """ - This controller sends click, double click, and hold commands. - No release command is sent. - """ - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "hold": Light.SET_HALF_BRIGHTNESS, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: Light.TOGGLE, - 1004: Light.ON_FULL_BRIGHTNESS, - 1001: Light.SET_HALF_BRIGHTNESS, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_single_1_0_0_0": Light.TOGGLE, - "button_double_2_0_0_0": Light.ON_FULL_BRIGHTNESS, - "button_hold_3_0_0_0": Light.SET_HALF_BRIGHTNESS, - } - - -class SmartThingsButtonMediaPlayerController(MediaPlayerController): - """ - This controller sends click, double click, and hold commands. - No release command is sent. - """ - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single_click": MediaPlayer.PLAY_PAUSE, - "double_click": MediaPlayer.NEXT_TRACK, - "hold": MediaPlayer.PREVIOUS_TRACK, - } - - def get_deconz_actions_mapping(self) -> DefaultActionsMapping: - return { - 1002: MediaPlayer.PLAY_PAUSE, - 1004: MediaPlayer.NEXT_TRACK, - 1001: MediaPlayer.PREVIOUS_TRACK, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_single_1_0_0_0": MediaPlayer.PLAY_PAUSE, - "button_double_2_0_0_0": MediaPlayer.NEXT_TRACK, - "button_hold_3_0_0_0": MediaPlayer.PREVIOUS_TRACK, - } diff --git a/appdaemon/apps/controllerx/cx_devices/sonoff.py b/appdaemon/apps/controllerx/cx_devices/sonoff.py deleted file mode 100644 index c7e8166..0000000 --- a/appdaemon/apps/controllerx/cx_devices/sonoff.py +++ /dev/null @@ -1,23 +0,0 @@ -from cx_const import DefaultActionsMapping, Light -from cx_core import LightController -from cx_core.integration import EventData - - -class SNZB01LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "single": Light.TOGGLE, # single click - "double": Light.ON_FULL_BRIGHTNESS, # double click - "long": Light.ON_MIN_BRIGHTNESS, # hold - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": Light.TOGGLE, # single click - "on": Light.ON_FULL_BRIGHTNESS, # double click - "off": Light.ON_MIN_BRIGHTNESS, # hold - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command diff --git a/appdaemon/apps/controllerx/cx_devices/tasmota.py b/appdaemon/apps/controllerx/cx_devices/tasmota.py deleted file mode 100644 index 322d285..0000000 --- a/appdaemon/apps/controllerx/cx_devices/tasmota.py +++ /dev/null @@ -1,103 +0,0 @@ -from cx_const import Cover, DefaultActionsMapping, Light, Switch, Z2MLight -from cx_core import ( - CoverController, - LightController, - SwitchController, - Z2MLightController, -) - - -class TasmotaButtonZ2MLightController(Z2MLightController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Z2MLight.TOGGLE, - "SINGLE": Z2MLight.TOGGLE, - "DOUBLE": Z2MLight.ON_MIN_BRIGHTNESS, - "TRIPLE": Z2MLight.SET_HALF_BRIGHTNESS, - "QUAD": Z2MLight.ON_FULL_BRIGHTNESS, - "PENTA": None, - "HOLD": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "CLEAR": Z2MLight.RELEASE, - } - - -class TasmotaSwitchZ2MLightController(Z2MLightController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Z2MLight.TOGGLE, - "ON": Z2MLight.ON, - "OFF": Z2MLight.OFF, - "HOLD": Z2MLight.SET_HALF_BRIGHTNESS, - } - - -class TasmotaButtonLightController(LightController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Light.TOGGLE, - "SINGLE": Light.TOGGLE, - "DOUBLE": Light.ON_MIN_BRIGHTNESS, - "TRIPLE": Light.SET_HALF_BRIGHTNESS, - "QUAD": Light.ON_FULL_BRIGHTNESS, - "PENTA": None, - "HOLD": Light.HOLD_BRIGHTNESS_TOGGLE, - "CLEAR": Light.RELEASE, - } - - -class TasmotaSwitchLightController(LightController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Light.TOGGLE, - "ON": Light.ON, - "OFF": Light.OFF, - "HOLD": Light.ON_MIN_MAX_BRIGHTNESS, - } - - -class TasmotaButtonSwitchController(SwitchController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Light.TOGGLE, - "SINGLE": Switch.TOGGLE, - "DOUBLE": Switch.ON, - "TRIPLE": Switch.OFF, - "QUAD": None, - "PENTA": None, - "HOLD": None, - "CLEAR": None, - } - - -class TasmotaSwitchSwitchController(SwitchController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Switch.TOGGLE, - "ON": Switch.ON, - "OFF": Switch.OFF, - "HOLD": None, - } - - -class TasmotaButtonCoverController(CoverController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Cover.TOGGLE_OPEN, - "SINGLE": Cover.TOGGLE_OPEN, - "DOUBLE": Cover.CLOSE, - "TRIPLE": None, - "QUAD": None, - "PENTA": None, - "HOLD": Cover.CLOSE, - "CLEAR": None, - } - - -class TasmotaSwitchCoverController(CoverController): - def get_tasmota_actions_mapping(self) -> DefaultActionsMapping: - return { - "TOGGLE": Cover.TOGGLE_OPEN, - "ON": Cover.OPEN, - "OFF": Cover.CLOSE, - "HOLD": Cover.TOGGLE_CLOSE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/terncy.py b/appdaemon/apps/controllerx/cx_devices/terncy.py deleted file mode 100644 index dee3f6e..0000000 --- a/appdaemon/apps/controllerx/cx_devices/terncy.py +++ /dev/null @@ -1,52 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, MediaPlayer -from cx_core import LightController, MediaPlayerController -from cx_core.integration import EventData - - -class TerncyPP01LightController(LightController): - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_single": Light.TOGGLE, - "button_double": Light.ON_FULL_BRIGHTNESS, - "button_triple": Light.ON_MIN_BRIGHTNESS, - "button_quadruple": Light.SET_HALF_BRIGHTNESS, - "button_quintuple": Light.SET_HALF_COLOR_TEMP, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class TerncySD01LightController(LightController): - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_single": Light.TOGGLE, - "button_double": Light.ON_FULL_BRIGHTNESS, - "button_triple": Light.ON_MIN_BRIGHTNESS, - "button_quadruple": Light.SET_HALF_BRIGHTNESS, - "button_quintuple": Light.SET_HALF_COLOR_TEMP, - "rotate_left": Light.CLICK_BRIGHTNESS_DOWN, - "rotate_right": Light.CLICK_BRIGHTNESS_UP, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command - - -class TerncySD01MediaPlayerController(MediaPlayerController): - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "button_single": MediaPlayer.PLAY_PAUSE, - "button_double": MediaPlayer.MUTE, - "button_triple": MediaPlayer.NEXT_TRACK, - "button_quadruple": MediaPlayer.PREVIOUS_TRACK, - "button_quintuple": MediaPlayer.NEXT_SOURCE, - "rotate_left": MediaPlayer.CLICK_VOLUME_DOWN, - "rotate_right": MediaPlayer.CLICK_VOLUME_UP, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - return command diff --git a/appdaemon/apps/controllerx/cx_devices/trust.py b/appdaemon/apps/controllerx/cx_devices/trust.py deleted file mode 100644 index 2caf213..0000000 --- a/appdaemon/apps/controllerx/cx_devices/trust.py +++ /dev/null @@ -1,39 +0,0 @@ -from cx_const import DefaultActionsMapping, Light, MediaPlayer, Z2MLight -from cx_core import LightController, MediaPlayerController, Z2MLightController - - -class ZYCT202LightController(LightController): - """ - This controller does not send hold action for on/off - """ - - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Light.ON, - "up-press": Light.HOLD_BRIGHTNESS_UP, - "down-press": Light.HOLD_BRIGHTNESS_DOWN, - "off": Light.OFF, - "stop": Light.RELEASE, - } - - -class ZYCT202Z2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": Z2MLight.ON, - "up-press": Z2MLight.HOLD_BRIGHTNESS_UP, - "down-press": Z2MLight.HOLD_BRIGHTNESS_DOWN, - "off": Z2MLight.OFF, - "stop": Z2MLight.RELEASE, - } - - -class ZYCT202MediaPlayerController(MediaPlayerController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "on": MediaPlayer.PLAY_PAUSE, - "up-press": MediaPlayer.HOLD_VOLUME_UP, - "down-press": MediaPlayer.HOLD_VOLUME_DOWN, - "off": MediaPlayer.PLAY_PAUSE, - "stop": MediaPlayer.RELEASE, - } diff --git a/appdaemon/apps/controllerx/cx_devices/tuya.py b/appdaemon/apps/controllerx/cx_devices/tuya.py deleted file mode 100644 index 6133c2c..0000000 --- a/appdaemon/apps/controllerx/cx_devices/tuya.py +++ /dev/null @@ -1,179 +0,0 @@ -from cx_const import Cover, DefaultActionsMapping, Light, MediaPlayer, Z2MLight -from cx_core import ( - CoverController, - LightController, - MediaPlayerController, - Z2MLightController, -) -from cx_core.integration import EventData - - -class TS0044LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_single": Light.TOGGLE, - "1_double": Light.CLICK_BRIGHTNESS_UP, - "1_hold": Light.CLICK_BRIGHTNESS_DOWN, - "2_single": Light.TOGGLE, - "2_double": Light.CLICK_BRIGHTNESS_UP, - "2_hold": Light.CLICK_BRIGHTNESS_DOWN, - "3_single": Light.TOGGLE, - "3_double": Light.CLICK_BRIGHTNESS_UP, - "3_hold": Light.CLICK_BRIGHTNESS_DOWN, - "4_single": Light.TOGGLE, - "4_double": Light.CLICK_BRIGHTNESS_UP, - "4_hold": Light.CLICK_BRIGHTNESS_DOWN, - } - - -class TS0044FLightController(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, - } - - -class TS0043LightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_single": Light.TOGGLE, - "1_double": Light.CLICK_BRIGHTNESS_UP, - "1_hold": Light.CLICK_BRIGHTNESS_DOWN, - "2_single": Light.TOGGLE, - "2_double": Light.CLICK_BRIGHTNESS_UP, - "2_hold": Light.CLICK_BRIGHTNESS_DOWN, - "3_single": Light.TOGGLE, - "3_double": Light.CLICK_BRIGHTNESS_UP, - "3_hold": Light.CLICK_BRIGHTNESS_DOWN, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_remote_button_short_press": Light.TOGGLE, - "1_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP, - "1_remote_button_long_press": Light.CLICK_BRIGHTNESS_DOWN, - "2_remote_button_short_press": Light.TOGGLE, - "2_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP, - "2_remote_button_long_press": Light.CLICK_BRIGHTNESS_DOWN, - "3_remote_button_short_press": Light.TOGGLE, - "3_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP, - "3_remote_button_long_press": Light.CLICK_BRIGHTNESS_DOWN, - } - - def get_zha_action(self, data: EventData) -> str: - args: str = "" - # Command is {endpoint_id}_{command} - if len(data["args"]) > 0: - if isinstance(data["args"], list): - args = "_" + "_".join([str(d) for d in data["args"]]) - else: - args = args + f"{data['args']}" - command: str = f"{data['endpoint_id']}_{data['command']}{args}" - return command - - -class TS0043CoverController(CoverController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_single": Cover.OPEN, - "2_single": Cover.STOP, - "3_single": Cover.CLOSE, - } - - -class TuYaERS10TZBVKAALightController(LightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - # Command mode - "brightness_step_up": Light.CLICK_BRIGHTNESS_UP, - "brightness_step_down": Light.CLICK_BRIGHTNESS_DOWN, - "toggle": Light.TOGGLE, - "hue_move": Light.HOLD_BRIGHTNESS_TOGGLE, - "hue_stop": Light.RELEASE, - "color_temperature_step_up": Light.CLICK_COLOR_UP, - "color_temperature_step_down": Light.CLICK_COLOR_DOWN, - # Event mode - "rotate_left": Light.CLICK_BRIGHTNESS_DOWN, - "rotate_right": Light.CLICK_BRIGHTNESS_UP, - "single": Light.TOGGLE, - "double": Light.ON_FULL_BRIGHTNESS, - "hold": Light.ON_MIN_BRIGHTNESS, - } - - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "toggle": Light.TOGGLE, - "step_brightness": Light.BRIGHTNESS_FROM_CONTROLLER_STEP, - "step_color_temp": Light.COLORTEMP_FROM_CONTROLLER_STEP, - } - - def get_zha_action(self, data: EventData) -> str: - command: str = data["command"] - if command == "step": - return "step_brightness" - return command - - -class TuYaERS10TZBVKAAZ2MLightController(Z2MLightController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - # Command mode - "brightness_step_up": Z2MLight.CLICK_BRIGHTNESS_UP, - "brightness_step_down": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "toggle": Z2MLight.TOGGLE, - "hue_move": Z2MLight.HOLD_BRIGHTNESS_TOGGLE, - "hue_stop": Z2MLight.RELEASE, - "color_temperature_step_up": Z2MLight.CLICK_COLOR_TEMP_UP, - "color_temperature_step_down": Z2MLight.CLICK_COLOR_TEMP_DOWN, - # Event mode - "rotate_left": Z2MLight.CLICK_BRIGHTNESS_DOWN, - "rotate_right": Z2MLight.CLICK_BRIGHTNESS_UP, - "single": Z2MLight.TOGGLE, - "double": Z2MLight.ON_FULL_BRIGHTNESS, - "hold": Z2MLight.ON_MIN_BRIGHTNESS, - } - - -class TuYaERS10TZBVKAAMediaPlayerController(MediaPlayerController): - def get_z2m_actions_mapping(self) -> DefaultActionsMapping: - return { - # Command mode - "brightness_step_up": MediaPlayer.CLICK_VOLUME_UP, - "brightness_step_down": MediaPlayer.CLICK_VOLUME_DOWN, - "toggle": MediaPlayer.PLAY_PAUSE, - "hue_move": MediaPlayer.HOLD_VOLUME_UP, - "hue_stop": Z2MLight.RELEASE, - "color_temperature_step_up": MediaPlayer.NEXT_TRACK, - "color_temperature_step_down": MediaPlayer.PREVIOUS_TRACK, - # Event mode - "rotate_left": MediaPlayer.CLICK_VOLUME_DOWN, - "rotate_right": MediaPlayer.CLICK_VOLUME_UP, - "single": MediaPlayer.PLAY_PAUSE, - "double": MediaPlayer.NEXT_TRACK, - "hold": MediaPlayer.PREVIOUS_TRACK, - } - - -class TS0042LightController(LightController): - def get_zha_actions_mapping(self) -> DefaultActionsMapping: - return { - "1_remote_button_short_press": Light.ON, - "1_remote_button_double_press": Light.CLICK_BRIGHTNESS_UP, - "1_remote_button_long_press": Light.ON_FULL_BRIGHTNESS, - "2_remote_button_short_press": Light.OFF, - "2_remote_button_double_press": Light.ON_MIN_BRIGHTNESS, - } - - def get_zha_action(self, data: EventData) -> str: - args: str = "" - # Command is {endpoint_id}_{command} - if len(data["args"]) > 0: - if isinstance(data["args"], list): - args = "_" + "_".join([str(d) for d in data["args"]]) - else: - args = args + f"{data['args']}" - command: str = f"{data['endpoint_id']}_{data['command']}{args}" - return command diff --git a/appdaemon/apps/controllerx/cx_helper.py b/appdaemon/apps/controllerx/cx_helper.py deleted file mode 100644 index 15c4862..0000000 --- a/appdaemon/apps/controllerx/cx_helper.py +++ /dev/null @@ -1,33 +0,0 @@ -import importlib -import os -import pkgutil -from typing import Any - - -def _import_modules(file_dir: str, package: str) -> None: - pkg_dir = os.path.dirname(file_dir) - for _, name, ispkg in pkgutil.iter_modules([pkg_dir]): - if ispkg: - _import_modules(pkg_dir + "/" + name + "/__init__.py", package + "." + name) - else: - importlib.import_module("." + name, package) - - -def _all_subclasses(cls: type[Any]) -> list[type[Any]]: - return list( - set(type.__subclasses__(cls)).union( - [s for c in type.__subclasses__(cls) for s in _all_subclasses(c)] - ) - ) - - -def get_classes(file_: str, package_: str, class_: type[Any]) -> list[type[Any]]: - _import_modules(file_, package_) - subclasses = _all_subclasses(class_) - subclasses = [cls_ for cls_ in subclasses if f"{package_}." in cls_.__module__] - return subclasses - - -def get_instances(file_: str, package_: str, class_: type[Any]) -> list[Any]: - classes = get_classes(file_, package_, class_) - return [cls_() for cls_ in classes] diff --git a/appdaemon/apps/controllerx/cx_version.py b/appdaemon/apps/controllerx/cx_version.py deleted file mode 100644 index 47290bf..0000000 --- a/appdaemon/apps/controllerx/cx_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "v5.2.3" diff --git a/powerocean_powerpulse/__init__.py b/powerocean_powerpulse/__init__.py deleted file mode 100644 index f82c610..0000000 --- a/powerocean_powerpulse/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -"""__init__.py: The PowerOcean integration.""" -from __future__ import annotations - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr - -from .const import DOMAIN, PLATFORMS, _LOGGER, DOMAIN, ISSUE_URL_ERROR_MESSAGE, STARTUP_MESSAGE -from .ecoflow import Ecoflow - - -_LOGGER.info(STARTUP_MESSAGE) - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up PowerOcean from a config entry.""" - - # Setup DOMAIN as default - hass.data.setdefault(DOMAIN, {}) - - # Setup device specific sensor list (used in updates) on HASS so it is available within integration (reuqired for unload) - hass.data[DOMAIN]["device_specific_sensors"] = {} - - # Store an instance of the API instance in hass.data[domain] - user_input = entry.data["user_input"] # This user_input object was stored after the device - # was setup and has the user/pass/serial info - device_info = entry.data.get("device_info") # This device_info object was stored after the device - # was setup and has the name and serial needed etc. - options = entry.data["options"] # These are the options during setup, including custom device name - ecoflow = Ecoflow(user_input["serialnumber"], user_input["username"], user_input["password"]) - - if device_info: - ecoflow.device = device_info # Store the device information - ecoflow.options = options # Store the options - hass.data[DOMAIN][entry.entry_id] = ecoflow - - # Forward to sensor platform - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - # Get the device registry - device_registry = dr.async_get(hass) - - # Fetching device info from device registry - # During the config flow, the device info is saved in the entry's data under 'device_info' key. - device_info = entry.data.get("device_info") - - # If the device_info was provided, register the device - if device_info: - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={(DOMAIN, device_info["serial"])}, - manufacturer=device_info.get("vendor", "ECOFLOW"), - serial_number=device_info.get("serial"), - name=options.get("custom_device_name"), # Custom device name from user step 2 (options) - model=device_info.get("product"), - sw_version=device_info.get("version"), - configuration_url="https://api-e.ecoflow.com", - suggested_area="Boiler Room", - ) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - # Unload all platforms associated with this entry - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - # Clean up hass.data if any reference exists - hass.data[DOMAIN].pop(entry.entry_id, None) - - # Additionally, clear the device-specific sensors list if it exists - device_id = entry.data.get("device_info").get("serial") - device_name = entry.data.get("options").get("custom_device_name") - if device_id in hass.data.get(DOMAIN, {}).get("device_specific_sensors", {}): - hass.data[DOMAIN]["device_specific_sensors"].pop(device_id, None) - _LOGGER.debug( - f"{device_id}: Cleared sensor update list for device with custom name '{device_name}'" - ) - - return unload_ok - - -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - await hass.config_entries.async_reload(entry.entry_id) diff --git a/powerocean_powerpulse/additional datapoints.json b/powerocean_powerpulse/additional datapoints.json deleted file mode 100644 index 172716e..0000000 --- a/powerocean_powerpulse/additional datapoints.json +++ /dev/null @@ -1,25 +0,0 @@ -, - "evSn", - "workMode", - "useGridFirst", - "evOnoffSet", - "errorCode", - "evPhaseSet", - "orderState", - "orderTime", - "orderEndTimestamp", - "defaultVehicleId", - "expectChargingEnergy", - - "switchBits", - "evDetectPhase", - "orderStartTimestamp", - "onlineBits", - "updateTime", - "evUserManual", - "evChargingEnergy", - "evCurrSet", - "chargeVehicleId", - "evPlugAndPlay", - "evExeMode", - "stopChargingSoc" \ No newline at end of file diff --git a/powerocean_powerpulse/config_flow.py b/powerocean_powerpulse/config_flow.py deleted file mode 100644 index c9bf830..0000000 --- a/powerocean_powerpulse/config_flow.py +++ /dev/null @@ -1,189 +0,0 @@ -"""config_flow.py: Config flow for PowerOcean integration.""" -from __future__ import annotations - -import re -from typing import Any -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError -from homeassistant.exceptions import IntegrationError - -from .const import _LOGGER, DOMAIN, ISSUE_URL_ERROR_MESSAGE -from .ecoflow import Ecoflow, AuthenticationFailed - - -# This is the first step's schema when setting up the integration, or its devices -# The second schema is defined inside the ConfigFlow class as it has dynamic default values set via API call -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required("serialnumber", default=""): str, - vol.Required("username", default=""): str, - vol.Required("password", default=""): str, - } -) - - -async def validate_input_for_device(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: - """Validate the user input allows us to connect.""" - - ecoflow = Ecoflow(data["serialnumber"], data["username"], data["password"]) - - try: - # Check for authentication - # auth_check = await hass.async_add_executor_job(ecoflow.fetch_data) # TODO what else is needed from fetch_data? - auth_check = await hass.async_add_executor_job(ecoflow.authorize) - if not auth_check: - # If authentication check returns False, raise an authentication failure exception - raise AuthenticationFailed("Invalid authentication!") - - # Get device info - device = await hass.async_add_executor_job(ecoflow.get_device) - - # Return the device object with the device information - return device - - # Exception if device cannot be found - except IntegrationError as e: - _LOGGER.error(f"Failed to connect to PowerOcean device: {e}" + ISSUE_URL_ERROR_MESSAGE) - raise CannotConnect from e - - # Exception if authentication fails - except AuthenticationFailed as e: - _LOGGER.error(f"Authentication failed: {e}" + ISSUE_URL_ERROR_MESSAGE) - raise InvalidAuth from e - - -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for PowerOcean.""" - - VERSION = 1.3 - - # Make sure user input data is passed from one step to the next using user_input_from_step_user - def __init__(self): - self.user_input_from_step_user = None - - # This is step 1 for the host/port/user/pass function. - async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle the initial step.""" - errors: dict[str, str] = {} - if user_input is not None: - try: - # Valide the user input whilst setting up integration or adding new devices. - # validate_input_for_devices will try to detect the device and get more info from it, - # and authenticate and deal with exceptions - device = await validate_input_for_device(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors) - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - # Create a unique device id as a combination of the device's product and its serial number (which should be unique on its own) - # The host can be udpated if necessary, as the IP address of the device may have changed - unique_id = f"{device['product']}_{device['serial']}" - - # Checks that the device is actually unique, otherwise abort - await self.async_set_unique_id(unique_id) - # self._abort_if_unique_id_configured( - # updates={"host": user_input["host"]} - # ) - - # Before creating the entry in the config_entry registry, go to step 2 for the options - # However, make sure the steps from the user input are passed on to the next step - self.user_input_from_step_user = user_input - self.device_info = device - - # Now call the second step but set user_input to None for the first time to force data entry in step 2 - return await self.async_step_device_options(user_input=None) - - # Show the form for step 1 with the user/host/pass as defined in STEP_USER_DATA_SCHEMA - return self.async_show_form(step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors) - - # This is step 2 for the options such as custom name, group and disable sensors - async def async_step_device_options(self, user_input: dict[str, Any] | None = None,) -> FlowResult: - """Handle the device options step.""" - errors: dict[str, str] = {} - - if user_input is not None: - try: - # Sanitize the user provided custom device name, which is used for entry and device registry name - user_input["custom_device_name"] = sanitize_device_name( - user_input["custom_device_name"], self.device_info["name"] - ) - - # Since we have already set the unique ID and updated host if necessary create - # the entry with the additional options. - # The title of the integration is the custom friendly device name given by the user in step 2 - title = user_input["custom_device_name"] - return self.async_create_entry(title=title, - data={ - "user_input": self.user_input_from_step_user, # from previous step - "device_info": self.device_info, # from device detection - "options": user_input, # new options from this step - }, - ) - except Exception as e: - _LOGGER.error(f"Failed to handle device options: {e}" + ISSUE_URL_ERROR_MESSAGE) - errors["base"] = "option_error" - - # Prepare the second form's schema as it has dynamic values based on the API call - # Use the name from the detected device as default device name - default_device_name = ( - self.device_info["name"] - if self.device_info and "name" in self.device_info - else "New Device" - ) - step_device_options_schema = vol.Schema( - { - vol.Required("custom_device_name", default=default_device_name): str, - vol.Required("polling_time", default=10): vol.All( - vol.Coerce(int), vol.Clamp(min=5) - ), - vol.Required("group_sensors", default=True): bool, - vol.Required("disable_sensors", default=False): bool, - } - ) - - # Show the form for step 2 with the device name and other options as defined in STEP_DEVICE_OPTIONS_SCHEMA - return self.async_show_form( - step_id="device_options", - data_schema=step_device_options_schema, - errors={}, - ) - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" - - -# Helper function to sanitize -def sanitize_device_name(device_name: str, fall_back: str, max_length=255) -> str: - # Trim whitespace - name = device_name.strip() - - # Remove special characters but keep spaces - name = re.sub(r"[^\w\s-]", "", name) - - # Replace multiple spaces with a single space - name = re.sub(r"\s+", " ", name) - - # Length check - if len(name) > max_length: - # Split at the last space to avoid cutting off in the middle of a word - name = name[:max_length].rsplit(" ", 1)[0] - - # Fallback name - if not name: - name = fall_back - - return name diff --git a/powerocean_powerpulse/const.py b/powerocean_powerpulse/const.py deleted file mode 100644 index 6ecbcc8..0000000 --- a/powerocean_powerpulse/const.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Constants for the PowerOcean integration.""" - -import logging - -from homeassistant.const import Platform - -DOMAIN = "powerocean" -NAME = "Ecoflow PowerOcean" -VERSION = "2024.08.27" -ISSUE_URL = "https://github.com/niltrip/powerocean/issues" -ISSUE_URL_ERROR_MESSAGE = " Please log any issues here: " + ISSUE_URL - - -PLATFORMS: list[Platform] = [Platform.SENSOR] - -_LOGGER = logging.getLogger(f"custom_components.{DOMAIN}") - -ATTR_PRODUCT_DESCRIPTION = "Product Description" -ATTR_DESTINATION_NAME = "Destination Name" -ATTR_SOURCE_NAME = "Source Name" -ATTR_UNIQUE_ID = "Internal Unique ID" -ATTR_PRODUCT_VENDOR = "Vendor" -ATTR_PRODUCT_SERIAL = "Vendor Product Serial" -ATTR_PRODUCT_NAME = "Device Name" -ATTR_PRODUCT_VERSION = "Vendor Firmware Version" -ATTR_PRODUCT_BUILD = "Vendor Product Build" -ATTR_PRODUCT_FEATURES = "Vendor Product Features" - - -STARTUP_MESSAGE = f""" ----------------------------------------------------------------------------- -{NAME} -Version: {VERSION} -Domain: {DOMAIN} -If you have any issues with this custom component please open an issue here: -{ISSUE_URL} ----------------------------------------------------------------------------- -""" diff --git a/powerocean_powerpulse/datapoints.json b/powerocean_powerpulse/datapoints.json deleted file mode 100644 index bd53e04..0000000 --- a/powerocean_powerpulse/datapoints.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "data": [ - "sysLoadPwr", - "sysGridPwr", - "mpptPwr", - "bpPwr", - "online", - "dcdcPwr", - "todayElectricityGeneration", - "monthElectricityGeneration", - "yearElectricityGeneration", - "totalElectricityGeneration", - "systemName", - "createTime" - ], - "JTS1_ENERGY_STREAM_REPORT": [ - "pv1Pwr", - "pvInvPwr", - "pv2Pwr" - ], - "JTS1_EMS_CHANGE_REPORT": [ - "bpTotalChgEnergy", - "bpTotalDsgEnergy", - "bpSoc", - "bpOnlineSum", - "emsCtrlLedBright" - ], - "JTS1_BP_STA_REPORT": [ - "bpPwr", - "bpSoc", - "bpSoh", - "bpVol", - "bpAmp", - "bpCycles", - "bpSysState", - "bpRemainWatth" - ], - "JTS1_EMS_HEARTBEAT": [ - "bpRemainWatth", - "emsBpAliveNum", - "emsBpPower", - "pcsActPwr", - "pcsMeterPower" - ], - "JTS1_EVCHARGING_REPORT": [ - "chargingStatus", - "evPwr", - "evSn", - "workMode", - "useGridFirst", - "evOnoffSet", - "errorCode", - "evPhaseSet", - "orderState", - "orderTime", - "orderEndTimestamp", - "defaultVehicleId", - "expectChargingEnergy", - "switchBits", - "evDetectPhase", - "orderStartTimestamp", - "onlineBits", - "updateTime", - "evUserManual", - "evChargingEnergy", - "evCurrSet", - "chargeVehicleId", - "evPlugAndPlay", - "evExeMode", - "stopChargingSoc" - ] -} \ No newline at end of file diff --git a/powerocean_powerpulse/ecoflow.py b/powerocean_powerpulse/ecoflow.py deleted file mode 100644 index 6179ee1..0000000 --- a/powerocean_powerpulse/ecoflow.py +++ /dev/null @@ -1,501 +0,0 @@ -"""ecoflow.py: API for PowerOcean integration.""" - -import base64 -import re -from collections import namedtuple -from pathlib import Path - -import requests -from homeassistant.exceptions import IntegrationError -from homeassistant.util.json import json_loads -from requests.exceptions import RequestException - -from .const import _LOGGER, ISSUE_URL_ERROR_MESSAGE, DOMAIN - -# Mock path to response.json file -mocked_response = Path("documentation/response.json") -# Dict with entity names to use -SENSSELECT = Path(f"custom_components/{DOMAIN}/datapoints.json") - -# Better storage of PowerOcean endpoint -PowerOceanEndPoint = namedtuple( - "PowerOceanEndPoint", - "internal_unique_id, serial, name, friendly_name, value, unit, description, icon", -) - - -# ecoflow_api to detect device and get device info, fetch the actual data from the PowerOcean device, and parse it -# Rename, there is an official API since june -class Ecoflow: - """Class representing Ecoflow""" - - def __init__(self, serialnumber: str, username: str, password: str): - self.sn = serialnumber - self.unique_id = serialnumber - self.ecoflow_username = username - self.ecoflow_password = password - self.token = None - self.device = None - self.session = requests.Session() - self.url_iot_app = "https://api.ecoflow.com/auth/login" - self.url_user_fetch = f"https://api-e.ecoflow.com/provider-service/user/device/detail?sn={self.sn}" - - def get_device(self): - """Get device info.""" - self.device = { - "product": "PowerOcean", - "vendor": "Ecoflow", - "serial": self.sn, - "version": "5.1.25", # TODO: woher bekommt man diese Info? - "build": "8", # TODO: wo finde ich das? - "name": "PowerOcean", - "features": "Photovoltaik", - } - - return self.device - - def authorize(self): - """Function authorize""" - auth_ok = False # default - headers = {"lang": "en_US", "content-type": "application/json"} - data = { - "email": self.ecoflow_username, - "password": base64.b64encode(self.ecoflow_password.encode()).decode(), - "scene": "IOT_APP", - "userType": "ECOFLOW", - } - - try: - url = self.url_iot_app - _LOGGER.info("Login to EcoFlow API %s", {url}) - request = requests.post(url, json=data, headers=headers, timeout=10) - response = self.get_json_response(request) - - except ConnectionError: - error = f"Unable to connect to {self.url_iot_app}. Device might be offline." - _LOGGER.warning(error + ISSUE_URL_ERROR_MESSAGE) - raise IntegrationError(error) - - try: - self.token = response["data"]["token"] - # self.user_id = response["data"]["user"]["userId"] - # user_name = response["data"]["user"].get("name", "") - auth_ok = True - except KeyError as key: - raise Exception(f"Failed to extract key {key} from response: {response}") - - _LOGGER.info("Successfully logged in.") - - self.get_device() # collect device info - - return auth_ok - - def get_json_response(self, request): - """Function get json response""" - if request.status_code != 200: - raise Exception( - f"Got HTTP status code {request.status_code}: {request.text}" - ) - try: - response = json_loads(request.text) - response_message = response["message"] - except KeyError as key: - raise Exception( - f"Failed to extract key {key} from {json_loads(request.text)}" - ) - except Exception as error: - raise Exception(f"Failed to parse response: {request.text} Error: {error}") - - if response_message.lower() != "success": - raise Exception(f"{response_message}") - - return response - - # Fetch the data from the PowerOcean device, which then constitues the Sensors - def fetch_data(self): - """Fetch data from Url.""" - url = self.url_user_fetch - try: - headers = { - "authorization": f"Bearer {self.token}", - # "accept": "application/json, text/plain, */*", - "user-agent": "Firefox/133.0", - # "platform": "web", - "product-type": "83", - } - request = requests.get(self.url_user_fetch, headers=headers, timeout=30) - response = self.get_json_response(request) - - try: - # TESTING!!! create response file and use it as data - with Path.open(mocked_response, "r", encoding="utf-8") as datei: - response = json_loads(datei.read()) - except FileExistsError: - error = f"Data from: {mocked_response}" - _LOGGER.warning(error) - except FileNotFoundError: - error = f"Responsefile not present: {mocked_response}" - _LOGGER.debug(error) - - _LOGGER.debug(f"{response}") - return self._get_sensors(response) - - except ConnectionError: - error = f"ConnectionError in fetch_data: Unable to connect to {url}. Device might be offline." - _LOGGER.warning(error + ISSUE_URL_ERROR_MESSAGE) - raise IntegrationError(error) - - except RequestException as e: - error = f"RequestException in fetch_data: Error while fetching data from {url}: {e}" - _LOGGER.warning(error + ISSUE_URL_ERROR_MESSAGE) - raise IntegrationError(error) - - def __get_unit(self, key): - """Get unit from key name.""" - if key.endswith(("pwr", "Pwr", "Power")): - unit = "W" - elif key.endswith(("amp", "Amp")): - unit = "A" - elif key.endswith(("soc", "Soc", "soh", "Soh")): - unit = "%" - elif key.endswith(("vol", "Vol")): - unit = "V" - elif key.endswith(("Watth", "Energy")): - unit = "Wh" - elif "Generation" in key: - unit = "kWh" - elif key.startswith("bpTemp"): - unit = "°C" - else: - unit = None - - return unit - - def __get_description(self, key): - # TODO: hier könnte man noch mehr definieren bzw ein translation dict erstellen +1 - # Comment: Ich glaube hier brauchen wir n - description = key # default description - if key == "sysLoadPwr": - description = "Hausnetz" - if key == "sysGridPwr": - description = "Stromnetz" - if key == "mpptPwr": - description = "Solarertrag" - if key == "bpPwr": - description = "Batterieleistung" - if key == "bpSoc": - description = "Ladezustand der Batterie" - if key == "online": - description = "Online" - if key == "systemName": - description = "System Name" - if key == "createTime": - description = "Installations Datum" - # Battery descriptions - if key == "bpVol": - description = "Batteriespannung" - if key == "bpAmp": - description = "Batteriestrom" - if key == "bpCycles": - description = "Ladezyklen" - if key == "bpTemp": - description = "Temperatur der Batteriezellen" - - return description - - def __get_sens_select(self, report): - # open File an read - with Path.open(SENSSELECT) as file: - config = json_loads(file.read()) - - return config[report] - - def _get_sensors(self, response): - # get sensors from response['data'] - sensors = self.__get_sensors_data(response) - - # get sensors from 'JTS1_ENERGY_STREAM_REPORT' - sensors = self.__get_sensors_energy_stream(response, sensors) - - # get sensors from 'JTS1_EMS_CHANGE_REPORT' - sensors = self.__get_sensors_ems_change(response, sensors) - - # get info from batteries => JTS1_BP_STA_REPORT - sensors = self.__get_sensors_battery(response, sensors) - - # get info from PV strings => JTS1_EMS_HEARTBEAT - sensors = self.__get_sensors_ems_heartbeat(response, sensors) - - # get info from "JTS1_EVCHARGING_REPORT" - sensors = self.__get_sensors_evcharging_report(response, sensors) - - return sensors - - def __get_sensors_data(self, response): - d = response["data"].copy() - - sens_select = self.__get_sens_select("data") - - sensors = dict() # start with empty dict - for key, value in d.items(): - if key in sens_select: # use only sensors in sens_select - if not isinstance(value, dict): - # default uid, unit and descript - unique_id = f"{self.sn}_{key}" - special_icon = None - if key == "mpptPwr": - special_icon = "mdi:solar-power" - if key == "online": - special_icon = "mdi:cloud-check" - if key == "sysGridPwr": - special_icon = "mdi:transmission-tower-import" - if key == "sysLoadPwr": - special_icon = "mdi:home-import-outline" - - sensors[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{key}", - friendly_name=key, - value=value, - unit=self.__get_unit(key), - description=self.__get_description(key), - icon=special_icon, - ) - - return sensors - - def __get_sensors_evcharging_report(self, response, sensors): - report = "JTS1_EVCHARGING_REPORT" - #_LOGGER.warning(f"{report=}") - d = response["data"]["quota"][report] - sens_select = self.__get_sens_select(report) - - data = {} - for key, value in d.items(): - if key in sens_select: - # default uid, unit and descript - unique_id = f"{self.sn}_{report}_{key}" - description_tmp = self.__get_description(key) - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{key}", - friendly_name=key, - value=value, - unit=self.__get_unit(key), - description=description_tmp, - icon=None, - ) - - dict.update(sensors, data) - - return sensors - - def __get_sensors_energy_stream(self, response, sensors): - report = "JTS1_ENERGY_STREAM_REPORT" - d = response["data"]["quota"][report] - sens_select = self.__get_sens_select(report) - - data = {} - for key, value in d.items(): - if key in sens_select: - # default uid, unit and descript - unique_id = f"{self.sn}_{report}_{key}" - special_icon = None - if key.startswith(("pv1", "pv2")): - special_icon = "mdi:solar-power" - description_tmp = self.__get_description(key) - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{key}", - friendly_name=key, - value=value, - unit=self.__get_unit(key), - description=description_tmp, - icon=special_icon, - ) - - dict.update(sensors, data) - - return sensors - - def __get_sensors_ems_change(self, response, sensors): - report = "JTS1_EMS_CHANGE_REPORT" - d = response["data"]["quota"][report] - - sens_select = self.__get_sens_select(report) - - # add mppt Warning/Fault Codes - keys = d.keys() - r = re.compile("mppt.*Code") - wfc = list(filter(r.match, keys)) # warning/fault code keys - sens_select += wfc - - data = {} - for key, value in d.items(): - if key in sens_select: # use only sensors in sens_select - # default uid, unit and descript - unique_id = f"{self.sn}_{report}_{key}" - - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{key}", - friendly_name=key, - value=value, - unit=self.__get_unit(key), - description=self.__get_description(key), - icon=None, - ) - dict.update(sensors, data) - - return sensors - - def __get_sensors_battery(self, response, sensors): - report = "JTS1_BP_STA_REPORT" - d = response["data"]["quota"][report] - keys = list(d.keys()) - - # loop over N batteries: - batts = [s for s in keys if len(s) > 12] - bat_sens_select = self.__get_sens_select(report) - - data = {} - prefix = "bpack" - for ibat, bat in enumerate(batts): - name = prefix + "%i_" % (ibat + 1) - d_bat = json_loads(d[bat]) - for key, value in d_bat.items(): - if key in bat_sens_select: - # default uid, unit and descript - unique_id = f"{self.sn}_{report}_{bat}_{key}" - description_tmp = f"{name}" + self.__get_description(key) - special_icon = None - if key == "bpAmp": - special_icon = "mdi:current-dc" - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{name + key}", - friendly_name=name + key, - value=value, - unit=self.__get_unit(key), - description=description_tmp, - icon=special_icon, - ) - # compute mean temperature of cells - key = "bpTemp" - temp = d_bat[key] - value = sum(temp) / len(temp) - unique_id = f"{self.sn}_{report}_{bat}_{key}" - description_tmp = f"{name}" + self.__get_description(key) - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{name + key}", - friendly_name=name + key, - value=value, - unit=self.__get_unit(key), - description=description_tmp, - icon=None, - ) - - dict.update(sensors, data) - - return sensors - - def __get_sensors_ems_heartbeat(self, response, sensors): - report = "JTS1_EMS_HEARTBEAT" - d = response["data"]["quota"][report] - sens_select = self.__get_sens_select(report) - - data = {} - for key, value in d.items(): - if key in sens_select: - # default uid, unit and descript - unique_id = f"{self.sn}_{report}_{key}" - description_tmp = self.__get_description(key) - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{key}", - friendly_name=key, - value=value, - unit=self.__get_unit(key), - description=description_tmp, - icon=None, - ) - - # special for phases - phases = ["pcsAPhase", "pcsBPhase", "pcsCPhase"] - if phases[1] in d: - for i, phase in enumerate(phases): - for key, value in d[phase].items(): - name = phase + "_" + key - unique_id = f"{self.sn}_{report}_{name}" - - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{name}", - friendly_name=f"{name}", - value=value, - unit=self.__get_unit(key), - description=self.__get_description(key), - icon=None, - ) - - # special for mpptPv - if "mpptHeartBeat" in d: - n_strings = len(d["mpptHeartBeat"][0]["mpptPv"]) # TODO: auch als Sensor? - mpptpvs = [] - for i in range(1, n_strings + 1): - mpptpvs.append(f"mpptPv{i}") - mpptPv_sum = 0.0 - for i, mpptpv in enumerate(mpptpvs): - for key, value in d["mpptHeartBeat"][0]["mpptPv"][i].items(): - unique_id = f"{self.sn}_{report}_mpptHeartBeat_{mpptpv}_{key}" - special_icon = None - if key.endswith("amp"): - special_icon = "mdi:current-dc" - if key.endswith("pwr"): - special_icon = "mdi:solar-power" - - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{mpptpv}_{key}", - friendly_name=f"{mpptpv}_{key}", - value=value, - unit=self.__get_unit(key), - description=self.__get_description(key), - icon=special_icon, - ) - # sum power of all strings - if key == "pwr": - mpptPv_sum += value - - # create total power sensor of all strings - name = "mpptPv_pwrTotal" - unique_id = f"{self.sn}_{report}_mpptHeartBeat_{name}" - - data[unique_id] = PowerOceanEndPoint( - internal_unique_id=unique_id, - serial=self.sn, - name=f"{self.sn}_{name}", - friendly_name=f"{name}", - value=mpptPv_sum, - unit=self.__get_unit(key), - description="Solarertrag aller Strings", - icon="mdi:solar-power", - ) - - dict.update(sensors, data) - - return sensors - - -class AuthenticationFailed(Exception): - """Exception to indicate authentication failure.""" diff --git a/powerocean_powerpulse/icon.png b/powerocean_powerpulse/icon.png deleted file mode 100644 index 4c4c498b263aab8a98a24b688badd960f83ac91a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4830 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 authentication failed or no data." - + ISSUE_URL_ERROR_MESSAGE - ) - return - - # Exception if data cannot be fetched - except IntegrationError as error: - _LOGGER.warning( - f"{device_id}: Failed to fetch sensor data: {error}" - + ISSUE_URL_ERROR_MESSAGE - ) - return - - # Get device id and then reset the device specific list of sensors for updates - # to ensure it's empty before adding new entries - - # Initialize or clear the sensor list for this device - hass.data[DOMAIN]["device_specific_sensors"][device_id] = [] - - # Register entities and add them to the list for schedule updates on each device - # which is stored within hass.data - for unique_id, endpoint in data.items(): - # Get individual sensor entry from API - sensor = PowerOceanSensor(ecoflow, endpoint) - - # Add sensors to the device specific list of sensors to be updated, via hass.data as also used in unload - hass.data[DOMAIN]["device_specific_sensors"][device_id].append(sensor) - - # Register sensor - async_add_entities([sensor], False) - - device_specific_sensors = hass.data[DOMAIN]["device_specific_sensors"] - _LOGGER.debug( - f"{device_id}: List of device_specific_sensors[device_id]: " - f"{device_specific_sensors[device_id]}" - ) - - # Log the number of sensors registered (and added to the update list) - _LOGGER.debug( - f"{device_id}: All '{len(device_specific_sensors[device_id])}' sensors have registered." - ) - - # Schedule updates - async def async_update_data(now): - # If device deleted but HASS not restarted, then don't bother continuing - if device_id not in hass.data.get(DOMAIN, {}).get( - "device_specific_sensors", {} - ): - return False - - _LOGGER.debug(f"{device_id}: Preparing to update sensors at {now}") - - # Fetch the full dataset once from the API - try: - full_data = await hass.async_add_executor_job(ecoflow.fetch_data) - - except Exception as e: - _LOGGER.error( - f"{device_id}: Error fetching data from the device: {e}" - + ISSUE_URL_ERROR_MESSAGE - ) - return - - # Fetch the registry and check if sensors are enabled - registry = entity_registry.async_get(hass) - - # Set counters to zero - counter_updated = 0 # Successfully updated sensors - counter_disabled = 0 # Disabled sensors, not to be updated - counter_unchanged = 0 # Skipped sensors since value has not changed - counter_error = 0 # Skipped sensors due to some error, such as registry not found or no data from API - - # Get the list of device specific sensors from hass.data - if device_id in hass.data.get(DOMAIN, {}).get("device_specific_sensors", {}): - device_specific_sensors = hass.data[DOMAIN]["device_specific_sensors"] - - # ---------------------------------------------- - # Now loop through the sensors to be updated - # ---------------------------------------------- - for sensor in device_specific_sensors[device_id]: - entity_id = registry.async_get_entity_id( - "sensor", DOMAIN, sensor.unique_id - ) - if entity_id: - entity = registry.entities.get(entity_id) # get entity - # entity is enabled - if entity and not entity.disabled_by: - sensor_data = full_data.get(sensor.unique_id) - # _LOGGER.debug(f"{device_id}: Sensor {sensor.name} enabled.") - if sensor_data: - # _LOGGER.debug( - # f"{device_id}: Sensor {sensor.name} has API data to update {sensor_data}" - # ) - - # Check if current state value differs from new API value, - # or current state has not initialized - if ( - str(sensor._state).strip() - != str(sensor_data.value).strip() - ): - # _LOGGER.debug( - # f"{device_id}: Sensor {sensor.name} marked for update: current state = " - # f"{sensor._state} with new value = {sensor_data.value}" - # ) - # Now update the sensor with new values - # update_status returns 1 for upated, 0 for skipped or error - update_status = await sensor.async_update(sensor_data) - counter_updated = counter_updated + update_status - else: - # _LOGGER.debug( - # f"{device_id}: Sensor {sensor.name} skipped update! Current value = " - # f"{sensor._state}, new value = {sensor_data.value}" - # ) - counter_unchanged = counter_unchanged + 1 - else: - _LOGGER.warning( - f"{device_id}: Sensor {sensor.name}: found no data for update!" - + ISSUE_URL_ERROR_MESSAGE - ) - counter_error = counter_error + 1 - else: - # _LOGGER.debug( - # f"{device_id}: Sensor {sensor.name} is disabled, skipping update" - # ) - counter_disabled = counter_disabled + 1 - else: - _LOGGER.warning( - f"{device_id}: Sensor {sensor.name} not found in the registry, skipping update" - + ISSUE_URL_ERROR_MESSAGE - ) - counter_error = counter_error + 1 - - # Log summary of updates - _LOGGER.debug( - f"{device_id}: A total of {counter_updated} sensors have been updated. " - f"Number of disabled sensors or skipped updates = {counter_disabled} " - f"Number of sensors with constant values = {counter_unchanged} " - f"Number of sensors with errors = {counter_error}" - ) - - # Device not in list: must have been deleted, will resolve post re-start - else: - _LOGGER.warning( - f"{device_id}: Sensor must have been deleted, re-start of HA recommended." - ) - - # Get the polling interval from the options, defaulting to 5 seconds if not set - polling_interval = timedelta( - seconds=config_entry.options.get("polling_interval", 5) - ) - - async_track_time_interval(hass, async_update_data, polling_interval) - - -# This is the actual instance of SensorEntity class -class PowerOceanSensor(SensorEntity): - """Representation of a PowerOcean Sensor.""" - - def __init__(self, ecoflow: Ecoflow, endpoint): - """Initialize the sensor.""" - # Make Ecoflow and the endpoint parameters from the Sensor API available - self.ecoflow = ecoflow - self.endpoint = endpoint - - # Set Friendly name when sensor is first created - self._attr_unique_id = endpoint.name - self._attr_has_entity_name = True - self._attr_name = endpoint.friendly_name - self._name = endpoint.friendly_name - - # The unique identifier for this sensor within Home Assistant - # has nothing to do with the entity_id, it is the internal unique_id of the sensor entity registry - self._unique_id = endpoint.internal_unique_id - - # Set the icon for the sensor based on its unit, ensure the icon_mapper is defined - # Default handled in function - # self._icon = PowerOceanSensor.icon_mapper.get(endpoint.unit) - self._icon = endpoint.icon - - # The initial state/value of the sensor - self._state = endpoint.value - - # The unit of measurement for the sensor - self._unit = endpoint.unit - - # Set entity category to diagnostic for sensors with no unit - if ecoflow.options.get("group_sensors") and not endpoint.unit: - self._attr_entity_category = EntityCategory.DIAGNOSTIC - - # If diagnostics entity then disable sensor by default - if ecoflow.options.get("disable_sensors") and not endpoint.unit: - self._attr_entity_registry_enabled_default = False - - @property - def should_poll(self): - """async_track_time_intervals handles updates.""" - return False - - @property - def unique_id(self): - """Return the unique ID of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - @property - def device_class(self): - """Return the device class of this entity, if any.""" - if self._unit == "°C": - return SensorDeviceClass.TEMPERATURE - elif self._unit == "%": - return SensorDeviceClass.BATTERY - elif self._unit in {"Wh", "kWh"}: - return SensorDeviceClass.ENERGY - elif self._unit == "W": - return SensorDeviceClass.POWER - elif self._unit == "V": - return SensorDeviceClass.VOLTAGE - elif self._unit == "A": - return SensorDeviceClass.CURRENT - else: - return None - - @property - def state_class(self): - """Return the state class of this entity, if any.""" - if self._unit in {"°C", "h", "W", "V", "A"}: - return SensorStateClass.MEASUREMENT - elif self._unit in {"Wh", "kWh"}: - return SensorStateClass.TOTAL_INCREASING - else: - return None - - @property - def extra_state_attributes(self): - """Return the state attributes of this device.""" - attr = {} - - attr[ATTR_PRODUCT_DESCRIPTION] = self.endpoint.description - attr[ATTR_UNIQUE_ID] = self.endpoint.internal_unique_id - attr[ATTR_PRODUCT_VENDOR] = self.ecoflow.device["vendor"] - attr[ATTR_PRODUCT_NAME] = self.ecoflow.device["name"] - attr[ATTR_PRODUCT_SERIAL] = self.endpoint.serial - attr[ATTR_PRODUCT_VERSION] = self.ecoflow.device["version"] - attr[ATTR_PRODUCT_BUILD] = self.ecoflow.device["build"] - attr[ATTR_PRODUCT_FEATURES] = self.ecoflow.device["features"] - - return attr - - @property - def device_info(self): - """Return device specific attributes.""" - # The unique identifier of the device is the serial number - return { - "identifiers": {(DOMAIN, self.ecoflow.device["serial"])}, - "name": self.ecoflow.device["name"], - "manufacturer": "ECOFLOW", - } - - @property - def icon(self): - """Return the icon of the sensor.""" - return self._icon - - # icon_mapper = defaultdict( - # lambda: "mdi:alert-circle", - # { - # "°C": "mdi:thermometer", - # "%": "mdi:flash", - # "s": "mdi:timer", - # "Wh": "mdi:solar-power-variant-outline", - # "h": "mdi:timer-sand", - # }, - # ) - - # This is to register the icon settings - async def async_added_to_hass(self): - """Call when the sensor is added to Home Assistant.""" - self.async_write_ha_state() - - # Update of Sensor values - async def async_update(self, sensor_data=None): - """Update the sensor with the provided data.""" - if sensor_data is None: - _LOGGER.warning( - f"{self.ecoflow.device['serial']}: No new data provided for sensor '{self.name}' update" - + ISSUE_URL_ERROR_MESSAGE - ) - update_status = 0 - return - - try: - self._state = sensor_data.value - update_status = 1 - self.async_write_ha_state() - - except Exception as error: - _LOGGER.error( - f"{self.ecoflow.device['serial']}: Error updating sensor {self.name}: {error}" - + ISSUE_URL_ERROR_MESSAGE - ) - update_status = 0 - - return update_status diff --git a/powerocean_powerpulse/strings.json b/powerocean_powerpulse/strings.json deleted file mode 100644 index 749956d..0000000 --- a/powerocean_powerpulse/strings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "[%key:common::config_flow::data::description%]", - "data": { - "serialnumber": "[%key:common::config_flow::data::serialnumber%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - }, - "device_options": { - "description": "[%key:common::config_flow::data::description%]", - "data": { - "custom_device_name": "[%key:common::config_flow::data::custom_device_name%]", - "polling_time": "[%key:common::config_flow::data::polling_time%]", - "group_sensors": "[%key:common::config_flow::data::group_sensors%]", - "disable_sensors": "[%key:common::config_flow::data::disable_sensors%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - } -} diff --git a/powerocean_powerpulse/translations/de.json b/powerocean_powerpulse/translations/de.json deleted file mode 100644 index d0fa882..0000000 --- a/powerocean_powerpulse/translations/de.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Das Gerät ist bereits konfiguriert" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ungültige Authentifizierung", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "description": "Bitte geben Sie unten Ihre PowerOcean Geräteinformationen ein:", - "data": { - "serialnumber": "Seriennummer des Wechselrichters", - "password": "Passwort", - "username": "Benutzername" - } - }, - "device_options": { - "description": "Bitte wählen Sie die folgenden Optionen aus:", - "data": { - "custom_device_name": "Benutzerfreundlicher Gerätename", - "polling_time": "Abfragezeit (in Sekunden), um Sensoren vom Gerät zu aktualisieren", - "group_sensors": "Gruppieren Sie Sensoren auf der Geräteseite", - "disable_sensors": "Diagnosesensoren deaktivieren" - } - } - } - } -} \ No newline at end of file diff --git a/powerocean_powerpulse/translations/en.json b/powerocean_powerpulse/translations/en.json deleted file mode 100644 index a9fa5b5..0000000 --- a/powerocean_powerpulse/translations/en.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "description": "Please enter your PowerOcean device information below:", - "data": { - "serialnumber": "Inverter serial number", - "password": "Password", - "username": "Username" - } - }, - "device_options": { - "description": "Please select the following options:", - "data": { - "custom_device_name": "Friendly device name", - "polling_time": "Polling time (in seconds) to update sensors from device", - "group_sensors": "Group sensors on device page", - "disable_sensors": "Disable diagnostics sensors" - } - } - } - } -} \ No newline at end of file diff --git a/powerocean_powerpulse/variants/dcfit.json b/powerocean_powerpulse/variants/dcfit.json deleted file mode 100644 index caab8ee..0000000 --- a/powerocean_powerpulse/variants/dcfit.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "data": [ - "sysLoadPwr", - "sysGridPwr", - "mpptPwr", - "bpPwr", - "bpSoc", - "online", - "systemName", - "createTime", - "location", - "timezone" - ], - "JTS1_ENERGY_STREAM_REPORT": [], - "JTS1_EMS_CHANGE_REPORT": [], - "JTS1_BP_STA_REPORT": [], - "JTS1_ENERGY_STREAM_REPORT": [ - "bpSoc", - "heatingPower", - "dcdcPwr", - "bpPwr", - "pvPwr", - "gridPwr", - "loadPwr", - "timezone", - "updateTime", - "timestamp" - ], - "JTS1_EMS_HEARTBEAT": [ - "emsSystemState", - "mpptRatePower", - "gridOutDayEnergy", - "meterACurrent", - "meterBCurrent", - "bpInDayEnergy", - "meterPowerFactor", - "isPvToInvDirectly", - "bpRemainWatth", - "bp1SocCoefficient", - "sysRunMeterFeedPower", - "inverterBand", - "loadDayEnergy", - "startDischargePower", - "bpDichargeAbilityAdjustValueLv2", - "bpDichargeAbilityAdjustValueLv1", - "meterAddress", - "gridFeedRate", - "currentNetif", - "batterySocLowerLimit", - "bpDsgTime", - "bpChgDsgSta", - "autoDetectStartPowerEn", - "meterPhasePower", - "sysErrCode", - "meterTotalPower", - "bpBusVoltageBaseCoefficient", - "bpSaveSocStopDelay", - "bpOutDayEnergy", - "bpBusVoltCoefficient", - "pvInvWiringMode", - "underVoltageProtectPoint", - "batterySocUpperLimit", - "bpRunDelay", - "meterBVoltage", - "bpStopDelay", - "errCode", - "gridInDayEnergy", - "sysRunMeterTakePower", - "bpSoc", - "bp3SocCoefficient", - "bpErrorCodeMask", - "workingMode", - "appCtrlState", - "bpSocBaseCoefficient", - "emsErrorCodeMask", - "meterRatio", - "sysWorkSta", - "pv2ErrorCodeMask", - "pv1ErrorCodeMask", - "manualSetStartChargePower", - "pvWiringType", - "emsErrCode", - "bp2SocCoefficient", - "mpptWithstandVoltage", - "meterCVoltage", - "updateTime", - "pvaErrorCodeMask", - "meterAVoltage", - "invWiringType", - "disOrChargeAbilityStepValue", - "reportCycleTime", - "pvInDayEnergy", - "meterType", - "meterCCurrent", - "mpptVoltageMinimum", - "mpptVoltageMaximum", - "gridFeedPowerMinimum", - "epoSwitchState" - ] -} \ No newline at end of file diff --git a/powerocean_powerpulse/variants/powerocean.json b/powerocean_powerpulse/variants/powerocean.json deleted file mode 100644 index 61a86f5..0000000 --- a/powerocean_powerpulse/variants/powerocean.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "data": [ - "sysLoadPwr", - "sysGridPwr", - "mpptPwr", - "bpPwr", - "online", - "dcdcPwr", - "todayElectricityGeneration", - "monthElectricityGeneration", - "yearElectricityGeneration", - "totalElectricityGeneration", - "systemName", - "createTime" - ], - "JTS1_ENERGY_STREAM_REPORT": [ - "pv1Pwr", - "pvInvPwr", - "pv2Pwr" - ], - "JTS1_EMS_CHANGE_REPORT": [ - "bpTotalChgEnergy", - "bpTotalDsgEnergy", - "bpSoc", - "bpOnlineSum", - "emsCtrlLedBright" - ], - "JTS1_BP_STA_REPORT": [ - "bpPwr", - "bpSoc", - "bpSoh", - "bpVol", - "bpAmp", - "bpCycles", - "bpSysState", - "bpRemainWatth" - ], - "JTS1_EMS_HEARTBEAT": [ - "bpRemainWatth", - "emsBpAliveNum", - "emsBpPower", - "pcsActPwr", - "pcsMeterPower" - ] -} \ No newline at end of file diff --git a/voice_satellite/models/hey_baby.json b/voice_satellite/models/hey_baby.json deleted file mode 100644 index 100a882..0000000 --- a/voice_satellite/models/hey_baby.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "micro", - "wake_word": "hey_baby", - "author": "Tater Totterson", - "website": "https://github.com/TaterTotterson/microWakeWord-Trainer-AppleSilicon", - "model": "hey_baby.tflite", - "trained_languages": [ - "en" - ], - "version": 2, - "micro": { - "probability_cutoff": 0.97, - "sliding_window_size": 5, - "feature_step_size": 10, - "tensor_arena_size": 30000, - "minimum_esphome_version": "2024.7.0" - } -} \ No newline at end of file diff --git a/voice_satellite/models/hey_baby.tflite b/voice_satellite/models/hey_baby.tflite deleted file mode 100644 index 52067b9410e35ca1148455beb2d4bffb53c7a470..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63520 zcmeFZWtf}Cx$rM$W@!bxf@Nj~ubJ776DM)vFf>ilw8=TCs-#URZJIR9jv+C|u!gnQ zYp<7?S*sN@GwVM-&3VuHzhB;u{gUUpe%C#74?g#d9*yp2MtleavUmN~G6p0a!iR7m z3j7lID^I-q z+A}Xc^~ljDo;>oa=U@Flc_$uCr4EDi`{gizU;OITmwxr?BgG*2fAYKq^4w2H0iJmg zB){^n_V$8!KHz>D=SV8`!cSp3kgYqm?_alj&!!#QAKA8UZ_C=2y)BS>kmvdTmFIx$ZJo${K_-GcoFg_$n)I)BhQN<-T!9@#10q%oCCZIcpk77kPl!0 zAOQ8BAdtI&ZvnppJPK$Aqyr!T^B*CQ0l*J{4*_d`3xSls34ut#%`iY`*FFj2Pk{J7 zkPdG6#BQJm=yyUOGdm!Vo+gkE=xG2Fuof7+ybk0q1Nloqz7jB=fVCVj7JzaVj06jnnm;sCcdH@}OHo!T+3BZ?t4*~B2eh>Ihz^j1g0Y?E30rmp6 z0@ecR0p);vfE2(7FaQJq0uX0`_5pT)9-suw07d{kfDS+#;2eMt2AKi)Jq5H2u!Bja z2RsDW3s?&#OFiHmF!Q^B{~q@-FgNedmxBNT00SZY*WAW{FsKHS4Y)tk?$7u8^X-1# z`}5j%e}Y4B|Mh9OpN9KM4+HYQ=VobPNo66hdFkW-YU6%=|M#{1f2EZcRu&fjuME%r zl=eF$1oFF|ub(aOvju*(z|R)=w^-l=EtQHs{jcXZ1pD*z_}?-JKR5cb1%9@`&ldRE z0zX^eXAAsWEzp*hO5tAq*LvtT;5X~Pt26cdRO*AD`tiGeV?X};{zpK6#IO9zF24iL zfdAJ2EPuAZzrz9#iBhS{zxruD0-x2tqYpo~`LhLnw!ptEU>B!SZh#*U2E+jn3HW>h zFaQF80$>0*06ricAO&Ot@&UzwazHhp9?%R}3)l$Q3fKwQ3pfaP2=FN2DBx+p^MIEC zuL6Dp_)oy^0KW(P3Ggo9AAk=5p8~!Fdh$KYRmBSY6y5Wfd*cQ4$FYeEM4%Gj4R)h!T+K|fY&WEazd$?5KozcGB` z>SG5}O6&Ma?@UKW`(+oi`}6!-Zpt*`TS{QC5~|G6edo;E$G=Sd$n%MGq=kX*<3CvL5{5tY+&$iWuN09H=4g6;P%4c#e;hUcK>N20bGm!S)#j!0A7G~E~>o@pCQ;(=S*K}k~ zQt*7*gBH4;LCl#5%rdTTy|aC}YwWp+{2BNkxZl^SWu2w(VD)sXl(NnuVzE4Id{I|0 zTJ!z#P}%WL^NWa@^9>63^}Z{J&r;{7ZWY{Z9Ngf@6y-Q9keVe&dTs(cUO3K}`fX?* za@RZDKL;;yE>$0^ULjj6IyOxaj2mG^j3$FsM>9!I75d!1qqWb^%0A+LTl{0Ts^;9@ zi}>Em3Hvx_A|Z4-=?RPy9;cpVeVQ9(Idk=C34S&o*GS7Sd$@{me|wuH2+b2^*_^q$ z4r7sqo<5Mzt109E=#Qj0R1of6yUeD1;jY6{(k2JBQsw!ra}dhL|Q4 z$m8y6$&;EUL+=fYyYD$xKF>-Mdb;89Kn^OqiyiWi28XxZ8cWRTn4uis)X36}m5;U8 z$37h})Z|CIr(mU|aB6olosn$|I({=ASxkq>(w9b$w`*^^lokW`SX|Ab;Z!QWFx4PB zu=WM~rW83XVeTd_4KMe8X~ILxf;7XRVN|?8%crUr0&l5(GOD4z4ViFW>xLIf;yyXRgV~B}#}bRYGbEzY9@J6pPcbF__w__b2rJ zc?1s6D)L!6G_VQZYN^^xp~TFvWH|~$gya+?5{Mhh`MB-;#01>wo}M`I$zacHU$__M zil!W}C@v&JsR$G-9wW%+qA9lJ8$({5cR)9-yfA9)7&Js1yb*@R-xb7S@vC;H8SVBp zgj{%n#mPXHI{N!9=M*Ddi+9`}f!-5ZaVEH>7@ge{OW-0Qj@w3|TCCZPJGQPRrUI6Q zWv@P_TUfMFmAr|ZTG%|f2xrHizc@5`NpWF(?zH9V{MbsDE@*WpYm(ABmJou=tg7D` zRYnEzxU}1r|{|N)Eep{A_?2(q2GbUPpfuyay`~Y4W@i!O-?(gP~@| zsqveuinCg_zVXXq56g^A+DDeCN=+{Jm4c0%wyt4jVT$xw3!FYxc+#AzeR@;F+MUqM z7N;O9sWVSmF{sK6epWKmNgss<-F@TzlREoi<~R#y?V}r-&{Tvci4|Zb-KG`Isd#{} z`Ga50gwf%!9zVA<#%g(9{?OCpswK8)ix65RgcW3^3O4Uc9^yV{ zaLoExV?65B$)%gt1b+X)HwtP>ET*-E$--PVV_hSzxOiJ^qhu|HdR;SP>J51V<2Q6N zY)M8lo$Co+Vxm(?p(jcx7a?@>ZHs3#$~$H~4CnSlgHS&kmIrZgPwBM?TSmy;wojg<7kk_!BMy5lvFd0Y^DG5Fa8cz9GaHA_pNEOiG1adAZiGyfEtGVpMMs{8cv~eC8 zTehP8(Rt&PW=`ERKjfsFp;nDw7sr67)90pzUoK~$*NaO0{1vm`f+jD|jd;coVJIw` zg2c^qZiq~2wrkJBZLjGoYMOaz_-YEC>>(xskPyR{uFDhBiWs#qzBDXj6stVfFh;rT zkaSyVIkX^(r>i5>#l>#4n~rG_ zS+ngKrjV#%=lVkV!!NDN%P-Y0N#5P4;lL`~96a4QUCgu zL!0^K!zpHB)@2d}+|iogX3l&W@^;tk0;!s#O3xLo+qUyS&ZGOEsz}1`YHq3q4QgfE zgg)RY&n~Do4lYgENr!8E%tSt~b<_4|>uVp4`<$oymsH(dLC3&{dVMxd=-nLh+!!0m z%&D&9B4b6=oP%AG=pzTKB;hdh6?Rto6HDeF62pS>H5>PsB{os))k66K_LGfq-Tak| z;-?=O6Q3On%XBq1m<@WPFuCEWBe~6GX|yuz!}PM#n$Iv;QNdz*&Hl*hT<qw85Ra#F&$2_jPn^rsJZYe23n9dah5~ zN_cjQ5Kc*Y$q=>|!r>Z>OCBE)8VeAxakqd5bzze>lE;^`q41!r9_39YoNBz5l}P!q zu7ru`O;|bbNHQ1gpx}VQ;>I5?dby%T9P&BP2!s(1%?XNQByGwINHaJ!4raL972l0tX?D~gKN{w*cVkkn^zyilHeXI1U%t)K3Q?FOqN|P zD3;-I%bpv>>fDH0wz@pIltj76I3_`Y%Fik&J6zZR54p$;6kQ#iP<@{I2JuNw%wQNq zPkD7XIFTd~qiTx^sxp`k)E8Zsm}=6E@pZm$Hxt z$8x;6VuTKfhfNx%hzoPjq!;gv1|pFR3)V&DGvT>mS*kFIgrZdy8Kh`_DOIgDr+V}Y zo<+9}$q{h)I8NNKDnL;}e0!GFX@`Tpzp9*&yQJY0$y@@0EhACVc;XxsR7-Sm3nGF{ zwt!Y3f4rPV<%*L@*s>pCvq4kZ06~piwR_W6d|KE524Tl=p{ywB%{*?LVJCzvgxFl% zfP3)jh}5i}l!rBxl7zK}jsB4RQ29cm*~bbE0ghumNvc_AhB5u7y6rXrY>} zVB(whf?6ZQdK0+7azWCrl2CN>RGimlB24t-i#yPn-&cCPJQx?80ZEFpCnx=D6_;x5)-^`7G0OzC~pq$b&`A9s`*y zLy61=s_fU~Rk9%|5GP_xNYH|^n5;|PI%iTHbrDerR}uoxMX?FNi~>2+#|!cb(?TX} zH0JSIi1DOHYlO@!ILylqA1Mxt;gZ4eLjDUZX1a@(O@v}2rer8BB*I|hDTl|W#~6K1 zECIzM;$jY6PSLz(W%_pDGK)ANoJ-Pl#KdE4A+Db9#knC>Tv=*;nk@N%On#*Pwteby zx9N(n+rPn}ko6B)M3{?Xh9A=-m;dSZH3|!rG`wz0&AiI@3o3EvbB*e zHzXbjL%eh>+%4X8;7EZ4nvj$Tm>uShVFS!sg64V4N=w5-(=L<_j*DRV*lhl$!qRgq(UJ*PV)DjRzN!Gb1NMQpC1;-lgbjb zNNUJow+KvGs2IfRCIt;Bb1zwu)O@asu|_Zi!*7n6)4|taVu*+^ITrKmL1p5$#LqOA zjWSocs%#@IZEIspwXo3OrjyeHX`>;p>rCwW>Q}m@l8yqH#)NYhj?bD&=?5tf6J-ug zv;rOAL4qV}_pI@P?pWgdSJUS62^oL6hPKH}xW2UeUYoglT`nO^n1cC?BZ`ixq{})wib>(J9kHm|-(%ofnrll1+hxDl z_F!4PBW_PYVJho!gO?HW%(wRxmv3dHYw`Zso8;Mqy3k+J`k(cc6>A9?uZ@xsS04|} zXPT13s%v!*Z>pranSn{0*po%vU?Uf|G(E6xd%wY%2`gl-zR$KaOHp+++Uy;bn9U~I z5lC{a9U+n8idUYe=O4n21=}!C3P~EBoG{U&os-?zuEOCV%NgwuR^a&3tNG#VHN+}| zW}Y*_&rOPSl(Dy`+a@ONVE(MTPC4%UZfUyT7M!ps&nvoS&F{?q)pBioI?IOluue^% zk$9bBqVClTwkb9fkzJ3gVB=lL1X>#!qs`;)Ix$dRJ!wy(`OHpUx?}6Csy7uCVUOes zvAekIaWB=#GBz$YdGSt0W(B*9yRN*|$S4&yL?p3LN-0VD4IsjU&qOUvAM>?{B6X&URwwSnY}jo!PJe} z?@UpR9}#4bw0Q1dMkKNNJJZ)J=pw)WwxOe7B)X7Aw%cU!yc`OdAI@3u zJjs>xO%hkkL1O{6n;jGpf95JXS!bKB^JI>>Zf-f4;FjYk$P5{r)w(&q za0go`qD65K6hBCD1coINm}4QVAMJE6+2#myOZwSFa@;vFoDAD$gKCre_Q<4W<>Yw$ z;=pvVWl7CZMNlL!apbOQ(&+J@7#jSWar3jSTvutFB<0g-9l6tf{L39=&zMN?$Xy)Lnr~Rbn%9MSD4t zCyLTchQ0tFLuuy1%NMW`63suX4aUR1K=w{7mYYv-Rch$1yAJU6>f13z;q*CtkuF_|ka9w} zwE_$o8Wn7==1F{mr;RzHScE!kx{f%ho*P{J4jRI`LKzHqLR+>iWl{+#^d<<$t{yZ+ z84L>|zF-!{G{NcXiVJB=P$(K}xY3i4EVX;0Q+&b4wL@gy;pg|Qq2U=3_8MxPbYJt9 zH*+($mx{uq^nLu|+VYaL2z=e7=c}T`LtA$hx~C;}PdRzr`bS^e^xA{`mZ+hOj4wl> z@Q5@DXg{Pk_+>pF`>chxK=#+2jv&%y+?mS6Ux2nk# zWf}sJ$rJW@I~q;ngvI)~yQj~;HTDJNYVSqb=pwx3ChaJRda)EjM$B5LjlL^OsBpJO zq3V{6-M(5W5< zS>6iV!S!!#t$eYdcx{Hm8k&pRl2$YLB=|*#h40hB7!D0g?U7KOo(Jr5=^gY zp?=ddRa;PrxjEO}9`y%sx7+ z<03`=B6JBE$6zCQ95jL0^LIxy&LJ})BwQ77^3&;5su`Y6L8qAdTM%^1hJC5s^}pG_ z6^d0Z7zV@IB|4wKjD%##9Olr)=au3pct$lPDL(-}xYF3ju1 z?iHI*mV=vLF$Or}I71Mkm2yb^)H?2l+yd^tYB@@f^xSBh)!1g;nL>hn4!C|}tGL;u z_NqgGpWUY792he)VScUz8$$4*`Iv2lvS!~eGPgjA z%3E`zi-GC!A#KnfqxwR=`P&ICWnOE7F5F8pP!~=6wrof6%PYzrqE#;~j_7nYUW&@( z(0#ExGvhWld_Knd=TGlg|JVNge^>CkpZfAG=s=O2w&j>Yj+wcHQ3^i#{8Pn*Tt0W8 zV!B;W5S4J&=j0C_7r&%<Q6PBZgpke{(SqdsxzKFbp7pz6zTc-n$at1kj!qzk=uU7)GtpcjyJuNv0pCF zBYnlr!OfW-R~)w=dP1S_$`$g{isFywJJ)d*VoXl{Ris?8owfbL|FK&RP^X%LQ$%vR|Z=#a*=douYq`6rhzUb#KL6}s<^ zxRf_~O^EEYlPV#1?!B0BT;wWlnmx-WzuYN14rg-ZyH~KSL7hfew%kS(wX1d)WRkG( zRE4fJw6I|HEimwI8ZVw$lO1}Wm7}>AYHQ~n!5qbrixsn?yO}%w_|6At`S>?ev$usu zk9~)l`NjJ3v}=FG%oeM3G1@igHnV%*dwq?&gt6(`5O+QanMzA3QM#|u8c zHsKCV<7!S=%Z^VH6kAyB`6=v%zb`)`{OtI;AG6k$N9^(MGZ9I7F1hCGe7@yndzPL% zH_7Tw64wZE)~UjKMb3U!I{5j~%TmP50n`L`2#UCsJ_;AuP4*BYKA#-9X?XgZ^QLW4 z`gKJlU~n`Cp?EwsS8chyLcse?SkwYU!KOG8yo}idmk_|UMw#&X0qtyNM2K7^>QE)Q z(Y^z-$NnfBn2H;12za*enQuSUDaJ&+Y<{ft+ZWC%DrZ?!I|||7_)3J|m8RV>6U~_G znz2#;Nt+Lq?Uornr#4@HIN7$UYk$H`c!iD9==*0>m*o1}ADugqkB+CAe7?NKUB{SRtS0;W1ID>yaO^P9xPnwPihGqQKJ{=G-Pgw4&g z%j;~bYc8H1rX4{V$4#-0HXnh~HON5qz74?Xnp3!f|ad|Od_ zYKmhETS$zgIdtCR3DD|pM#=U+;2n8u6^ot+D3`dXZI$WCx%! zL5g~Z82Qn0YdI#?=E}9DDuc6lQo7EcQAwT8V36ykv|dkf(XSVGby!jGGZC3mqDwdvQSkhB1NnJnvf z;HWDLVF6aW-Y4v*mNcpDiX%3sa(OpHfUg9;}youB-Wdjlqa(zx2`m6~>k zT-=f`9BjtzBJkGk2-Z$v)EY9;Z*hM$BxjO&W#V<*g)JBE#SkA}#UxqRYgNC#(cXVd zF|ivq>%pgKr)r6GWQC1I*92W{6extVqkM1$b4<1@*N;;xh{Ms+qN7L`b+XWpkdQW$ zJN>t(C!}^x6x9ITd$*i7rhqiAeUOQ{wsj(K=H|NMb6ULYeSu*Pzw+eQ$+IQltUhL$ zJah0r94MQrY_fAYovWe>hxjO#qHRasyW5t3jYBg6NJ)4B%37pnY#V`YOi{nPTsOFs zQL@5UWb?#de^I&2Ar#0UTCq3+Wx;(V&MKX;oKmkLi%~;zgHBch%K;b z#P;=;4OwUF>etHiLo5Dp2Hq=BUid(Y6RyciuU5@6e%}hu?{3e zay)J{f8}PKBLs!%$SZY_zE>5RLj~kAUf<+dC0yl3gQC zM7yEadgVGEw$dp7GAr}fgedX>3?|UQr4Hn+To@SJ+v78rXrR)wV^I$&-DgzDxC`)& z(H{c@D#ZCqz0}+4L6KcihaVf@sItO`AIO_u-)Mz@fofc7D@FcOelzqS{fwjEU!ZM{ z=ES~&$!9-}y4%!UqhDGw`+mJ!xlX0b-=lxJ6++M`qNh^36a9YoG;1=(iyR65c$Hwi z8bmo%hO4bq6o(wTRXs@B^G)t(VDz0uwsk??tXhX_!?tqDvdjhH5_-y5`PV@&l0Ccba|ogh#0T@QPzCzZFax@*aGdYb)Pm*kt#qNGorvufn; zjh}V!>EzTu8O*q&K1S$y3Z4lw=(-ds6p8R3Bi5?sUiVgOSq8=`yisG7*r9AEY5VN_ z0|fb|q{*qu{UNqprh*eR(*=G7bnbTfdCgAT`fqHl&r({7kza282)n`aU}yH56n`3z zGxT;0gScsP>6c8}H(!P0Azcm!e#&%oBssm?trl0kV-Xx za4qNuQ3v+sfQ3v3(0vTrdesf~`+$Dl0`2Sp{U`(rp9Dy62X($02K$6Sol=l41%0_^ z1^anGzg__cUX4DmUkJ3p1$FENbp*ko?j_L18=#&8pv)vl^M3U&Tc|-@w{|_OSW#r* zi+21iT+ri+E55%=l|4OPoF`}K7jMjZcQlTEvh=qHt1{w>cQ>E6Z@GN?6k*+N**fjM zXDi1E4qAS?!dLl0{e~$WPSI_Q)>F|_mfXYe=Th6`s;}+GMdiQl-Yx%OM`28t=1YEv zdSN*c7vTJZ8|~)TU^@3Vf;Z%D8V#e7tEObaZM_>_myQWp6&YAt!L{RC`EtWA<+~+S z9~o}${p8r2&&!#@JOq#b;B$`=PCON^QpndR1L7yanCt@aF@OQ+?w^O3gYZp|wg7kwkOSfyK^;p#KKsqT`tm3UGd8{L zIi{$UKYdy;x4uBM0rVNu`a0*EJAc?V))(Hh!LsEu)U8Rz>*e2!UC^%E(zN5TeVBtg z7z)Ec$-)RL!O<)K;+}ge|HZPET#e$O^z}#gts_%5kp3=Jq}^SYMi#rHSMOfy^+h|= z5W1694?px~RiaH9`$aqcb=~RbHjFq>vn#hgKc==EUu&=*uc>-2k7M~D>nnK*+h`j4 z?SXH*Yw+D9()s=~ii2O^FBN0pm2ne%$9KCcT??jv3gmAu9sI|67-5=m^)vhrfs8E; z3Tr3#Fif?pfjo5qlFWWbv3~IMQ%~&NtpAa65wp{`bn0_upSQ)lS~a+qkkpal3!{O5|ZfPqRzD#(dUTa;*2*`S$1J*@wOxmOn>D^u0c@-e|1( zmeCYTk1(&i*X-G(Kz!Zst19{YMKV^ADV2+{`Z~Nq4j=g5DOV_vH;{AR{?E9B>7LpS z#^xjN`N;#}9uWQkjKOkh5$KRW zehuXPgX6n^UI&;5c}@fHK!3kpCjh7+j{+V$_OHI&pK#4oCYB`)-dZy?qfy0F!C{|h z;k|pwcegndLh%Sm8&oY@?YL#8bm^gT98BrNG zZI)temT3v!;@nzf%4Kv?++?q|Z*B#9g|xaTsy~myu=S76tjtNbkso_lL!No`iGKfw z^5V4i(r3PGnX_LL7RlIlWd?eUfghTBpE-$w-rhK7c{uCqg+q)GaSh3^hCG5pK5kKz z1QYa+gx8$-$Cf8?{gZuVcIj$FQ$2?qz2`H|(mtCxBzShRI_#wD4t6Amr12Tp)5~Z7 z<}FO%pQ|{%y<91uI^UmwC|}kvYK1sT=#x$B`0wIb%hKFjAQzFV~X09P;pL`a9^ey|lrgHy!G2bcG~0Gk6Fave8m5+}{2NnL|jAV0fBr z;?1XuzQU~NZa=i=>1aJNr=*6rE9JpYT`iVS3doGv@9lC+btaleS0Wr`k$(TWSp>3G z5OUDlwGXoBoSiEkV`P)QDimx+Zy{iu@tg-u)^+!nB6B6*H`=@U;Vy3HOf1q;m7`c$ zMcK1g)P7&6K)1M<$LV!7_qh^z(UI@DKSt^0iAuX$SczawXAO--=nO?+!Klc7vk_M8 z*tig?Yzv(z88Ts553?^^&S6DrOP4Mm-f+YSv7)F$!4Tw6SJt8Vc$N#xCbNr(OP7N7-YG8loRf z39OsDTd6%PsZ*CP+kY)kxl}kEdXaJU_s$<5-L5p`2kOT+pYRROCk_(xa(!c2LRfXBls(rI+`F(O!s$P6ADg94q^+T;7H*VBFxx~2 z6fjZfGN~M;`I= z(}%}MXBZUjJwCL?Xpu`OlV0fHg$?nnB^Rl9+xSF!9ff+Q?reOP*@MY!zJzB9>*dM) zCrIm8o`RLt&uxd66kW@4`Cb)lDk)_mX?|ZHn(Fi&bEJMF-XD1`sTMZkq`@6`vG}>g zzpD=2n?P!<)FbQ}(ZgJp&%Wa!EMB6VeP4_oWngx3NiF=j&DV5>0gth@XxK@cUEFjrh& zO{XiEtFOo-k;C%7{L=;NHZ_cw-=M{rdC*804q-g>l4xcwO$<3eijT-{dH#mV_n%a= zJAaR%G@<{PI*FTFlmGH`(TZVS>E9@5QQj;V-d5Vm4j-8mD>~mi3H!+TSX^+zuH-t_ zp6T);eqb$FJShAa;hTqdE*Z3WqigIgx3l}f;;z_G zYk3I9blK&OPhGR3F|0bYmvE{?;hFwz`p6 z`DBf+yxSE!NBZ(1Y}DztHKL!WbQU2OyOXZbfNjP}&R!gVG&tUa)G0*s9VeZwgRb$# zTmD%FTvt^`OwpEoScSEyM{*uYN{bLoV29!4u^d_eamTr^9K|n+7=1wrgs^-Wt4~y* zZYEfFZsi%;?rF>GVdm0wwea((u2uH{SvNaA@|#!5NZY>9m9bp6Yn0B&(#=<3I0~sl zzXEA0-80r%*P}8{;G7z)iYWM@lY1U zQVDbL4M&&T-dVBX9s31&UGoKGiblvoc#sZ42?L8k(!u^fh@UW4A7cuuQ-!%q?2sc9 z<%wb9kbDA+Npm5ig2nNGke*0ISN%w=!ar!qM0+6_ML$y7cFo^{=B?14BhRdek1n5H z4&Cdv&F%Pbdd$2~rmQ4U-`PnYmqq319be3!8a!RIy>*!kW1i#Vo(jNui#wu?V>Hye zGlg%*`6(KoL`ig2`VYa7GlPZpnk>QyoM{(%k}Ept#E&te&UI4Y9=s{dl?=&q9&@l} z_UPuXUO6w5`sGX_BLtx=8qdP#?Z)49ty#5oKi&9yDaNVmeEi1p-RNparKW6YzIylM z*8@v5U0OK$-EEd{IR34osA~s8CeF5sAEzIdt)^QS@`<&DXWaCbiE!=CvX$&r(I%s2 zprIZbleWILf_u(od30{a>Bp<}@gL#M7?jVRKd?6FY2lE=n3zqj;V&l$3CZ+kTtbGVwdFe zPaIWW!f(Bsc=}rXFJb$K)>Y)_vbnN4$mYuN+%WD*Gw)9eDEJa@ZARTN^KQ7s#pjwa z=%SAFENq7&w_LeKQ=&7>^APFmshkIl2yT>kE{)oHI}OHNu|so9wr}(jzYHZ!{SWLO zzuj!k-o}r)irqHU1?UWhw_Hu;X(lgEE&D~0E%J}a$eN|zSR-%e`qD)!ml2$8%guum zpw6y-%Nk8#!bBEknRviD*En-(we0SLN$NdOiXO8P7 z4~9^+3tkO%A5{ld7W}3n=-vZ|`piA5OPq~EX`x8wKbytQKmJFiO|L1XFQO^F^n1oo4dO=!M(aDxo)jiw(_DwnIgvLRsHZa1qv^3|tY$c6 zSxSlsVt#;AK8TeeipTqGWR}V?u;t=T^(JG<6)Nmrw!|3-!KmxT+vVw{<>L9YK#jDz z+jz3um8*d!_GygCe@=H1stT2>nM~~+=bt`y5a)!9%n&6WEKHlBVJZzV1=De^ZN)zw zdZ@enQ8(dJ2ZRnMPo<{odo4F@@x#U9Vm|dIhv7Q*$@a6XOvRvvvRC4aOZ%Je>{%FI zouysRlsnM^dm2ZAYM_6sw^&GGHHBRZ>6AMx%^DiMsfe z$8Tqw@lS2N*VZ;WPTW1;TV`ixu^wG_NOdRY5AtY6g8$cSmo@;QG^$|TDhP@u$_ptz zTUnKSAGMWt-H>eN=UtRg-JDA5*!%If*1z~Yc}^lY$DBLzlAh3*`oZF*BrC`EHsNO? zzGVltD*ay8EoTSK%nNQDKx=s7f$YF6y2-Qnu_t7Y$AgztV*7yKoilZ_zhxUkj$!Ei z+IA?eh_dbPw7V;sk?nSUSvD3awdOCRGkNNhSy>r^^Re`)usgFgYT7kskk19-a{_+e zTP3}ruOrgyFYU~VT@K+f0j0AiVNhS>6(g)*(GhR3OqC1wW>BeiULR_;@R#n*}Hw1dt zAu|YH3}AR+T{}ekZ8hE zbPM-#P;qi~_S>Y2|F~9CGWC5#lKXel$y&Sg=*yFWyNkYb@oWLqK7l+wu>9e7NnNM{ z4!%!2cZvi&vuViHAH^(=uc#Y}en~p zm-Wforc)+ur6?({zP0+)p}>b6?(Ol6sz}$xZhGriOCw`{HOrh02VNat|5Ci#yi2y1 zO8G@dQ_&>khC3jqS!GdA?_?+@Y%(yPfjNi6K96)dL35fH-Q3>Q#F6kJJYCv|Z-C1S z2?9u%iqnPZN~81XnvyRzZPOD9-&35qcFQ-rV3^tfyEid+@5w>#@Mm%)a}b9kACR0_ znNv#!)=!9b#Y=M*!}|V<6kIah6@!Y6{Gg+5XGZzvbnqoZXbNC6MFh8!fT(lPOHi5n zHpLaNeGw$x2=k6PLO7FKM4u@d7GqesC2SJGp(sWtyVR-u(vWeSIK`07(I-NvvXI7T zD5^=Rs5-$Yq$Gxdt&hfKSNO)IVFb%RIOH8M;~}uiFJAh|7XW+*UEt;CPT;qD0{G;x zKz;yxbjOxcsSMzJXafF0B5+Kd2fn+9mOdxz7{863O z0{+Vg#M?kQnlqL10KecN@bVP{d=X2)zi}DFHvu1FAIS4CXmdSidk3Htv|$EqE&%`G z8PLWe@K5Xq{)$BqJ`4H-0d0N?{E2E%?svd{=>&D(1nK>tA9LDNstnY70Q6z~D)4)N zdw2%;KPNzYVo=X3pnf%|XAR)KAM`_z<_F~)Kzk{mI|u4~hsRP z3yMwLR<(bLCRbw8y)JL>y;mhw%P1z6p@C~ts{uLj>Cw;%FT?knNm!^O>YNI0M3&Bb zTZ&NdXnA1l+u5$B6>SdH6U`e_cYlDZ7Jr)6A|AbwE_TVeuW>q8PZeZ3dMnwY4hs4B zs6tZj(1M-;c@9DHQW>6#SW9dg{vm__Jz&J#T=qeGL!<;aVWU6iD6oW32&k2|x{a8qf!H z8-P3yig%=n8U zz?+bs@%@a&a{^SYO!w4{_={UG+-ghi{y^ z$tJsv1hRZ%ho0+rrMVdT{%t|qXX!*wpz)dQZBHx2E#9b2jCky*i%{=h|7;=St_M!L zvo4Q^V!UwZtCd0+W@Ox?p8#r3a4)1}gwg0@Aq0m0rkU*xX&RLvK?F|lob3@aZw z3F}{86lO&`Z?V0tvaum8*=+2 zPq;DBJ?W1Wb@<>Z`NN!NR^<)*wAWS@GgieG zTJzc3?-k$b<%;k@jquw4%)2+hykG)K!MNXdZnlDX0Ui&j0FZA3dH;W2Ujn=Z;;SHj z1cbpO1Tq4+?;qU*Bpj4wf@hNqJSTR8_^&{|DS-2*d{H35E18rGNbr}Tko)tM1=tHX z^HX2$`%y`?b5I3%J{GGTI@iYAgz*gt9^}u*ya}7BUEejTsm?kJ4%T4{yaEe!tw1^L zac6gIRSG1&c5zOI?gzNyP$9F->BzbwPO-rQB&Hgt17*|@lz>vv{Uh6b2M zhn0;+`j(^lqTE2~`HU}klEQRLBu2)rcHL+mo0&z+)?a_3XVAup%I)5v ziGj!>yy6(en`FC5KBF#v+RB$T*r{Ewvh;G`pOOHQobVW zK8_ts%`(8r92wzgv3n+Vpo#|t|81x5rf@Lo2^LO5fTQQhd9q`K z!^_%^Cv(+wIg8TnUgpUnYnH5DvJeB-C4_L2EK;u3A$?vRT07B~S>2I~5@Y=oNbc8< zjApJ@(tGFf30`Ak8ssaflv3h&gs0<@l;2k5TtzT%5PF;*9tr`+RD{y%GHnVsnPEaX zMc)~1^`JzUHlN8Ltdw;9brOFlH>Js((<((dJlV4_}FKBanbKcrw)5AH>R7ZbH42Rn@oMb{<=G{_$soY*hO7gbm z{0n}PWPx)mx#-flSd=8{H(lx(94kfG#HmO&HsbeGgwU5lInBzQ?`u%99zqwDGKH1Z( zM;%BfeJ(p|$B{CUjejl=zkAh5@;SO8VU0k_rbJlZLGbhvG^VNn!jV&JJrbn>IuykJ`62FP}G?urMKe!3& zv`)Znk6hB<2shzOtRyiQwNhRv4tlSCTw>3$@Gj)yXOlW!c(IdsQIw>y zSreg)IA<+y)rb4vXnLn^0XP83jK;%>g9DdQ5Fg4_C;(aRfuagU3Z;>sQ8@+b{qP2mbsrLJkPA3?M|B)XG6i^b=lZ||(` zEVCfHM3&UJL&@L@Idj>lb;6?VnLiDf;-fj$Im(p$+HGl0gV&9F5q73rSw`r=@6dIi zH?LB%IU(`tkiN|O5t(;QhnYIe5mMSA?mLm`v;QA^Zyg`i@%<04TXr|@3E8;2us1h^ zKydd`prue~OG|+Q)lb`WsXKL}Mxn*MxF&=Mad*#Vv%6V$ea<9AND2KufB!nK*Zazx znK@_9oblPabNLzKs-8){@uD=vQog*U!WtI1MIijsRK99J2o-B-FHC;-MSRnm^`b?NgT-UO4m1vcKl@YKi>A=M$sYW=OdcCqF`1`Ep zwxB};u^wEWh!AIf*k6@kb0(H`@-OAP+gHeY#8$zo=Q9b_lVo+R>b;^z zzY90V6jWSZ5EmlGx|C+24=tl;wI3^0Whs5>LVoq6*wdY~p&yWftDNjl6tNVX*lK}^@ukI)Ztcvw8eH;cJ&W!5 zN@Co;c4b1uG`gMA5D|AkU)Og6vw4(cU!dDN{L|%D$;lHlSjGWzf6VVjFI(Y^sfILb z#MSWp7Q98sDkNYAUV3mwP#wiUyNT%e_G`fLuzOx zHyZY!RHZ-a|C%cjM5MS_G-6vvHzP82ABEa(G+TG8%q^bVC+BmAEHm8$KO{NLmo%|e#9Y!vxk>J^I;gJ78GuKKu+}P&r zoN|e!c3VAzz*_K~F+FZxYE=w@wLX7GfjIKJh$ookH{u$+N&N!1^FrD;k3@7j1Q)e? z_4}8!-sN7pB01OZrK=ScyQFjn>Kr|9x(B;1`&?h&-WpOW3(IpH=-;V4KD)9r)2ksZ zHTG@}x1J&V)i|%tE{~iXMQP!(^pjj;F2qC&=q&@Dp$X0bSFiar*onn7yGLS0d04*o zquU?OE^nG+FLK#`#-62#qBHp{hfaqU2VGD&b~!LyU+651R#ucm=g#)9#ctEXpnjmH zC$H1CoH)sOk})(WQCz#Bff|ttKY)Hp?w8iT-!4e-5=$H^QhIOIG34)b)|*;SAC~`AOES|_ zU0V}WTikYRt!{H%2X{c~veBAUc;#9eJLj^$eLymKYNtcx%!-SI^Xe8q3qjP?E@^N~ z^qbe?-q$51eO-JgY3c$A^I8M*d|U=ONHWDC)gUHqa

?S^UI)eFq~(^~lU~8DZ7a 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 IlNx^iFI!#f6#xJL