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 4c4c498..0000000 Binary files a/powerocean_powerpulse/icon.png and /dev/null differ diff --git a/powerocean_powerpulse/manifest.json b/powerocean_powerpulse/manifest.json deleted file mode 100644 index 328b9d6..0000000 --- a/powerocean_powerpulse/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "powerocean", - "name": "Ecoflow PowerOcean", - "codeowners": [ - "@niltrip" - ], - "config_flow": true, - "documentation": "https://github.com/niltrip/powerocean", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/niltrip/powerocean/issues", - "version": "2025.01.07" -} \ No newline at end of file diff --git a/powerocean_powerpulse/samplereport.json b/powerocean_powerpulse/samplereport.json deleted file mode 100644 index 3091c51..0000000 --- a/powerocean_powerpulse/samplereport.json +++ /dev/null @@ -1,635 +0,0 @@ -{ - "code": "0", - "message": "Success", - "data": { - "sysLoadPwr": 823, - "sysGridPwr": -13, - "mpptPwr": 0, - "bpPwr": -836, - "bpSoc": 88, - "sysBatChgUpLimit": 0, - "sysBatDsgDownLimit": 0, - "sysGridSta": 0, - "sysOnOffMachineStat": 0, - "online": 1, - "todayElectricityGeneration": "95.30", - "monthElectricityGeneration": "192.67", - "yearElectricityGeneration": "5298.98", - "totalElectricityGeneration": "7243.73", - "systemName": "Florian Krebs", - "createTime": "2024-09-21 20:36:04", - "location": "Germany-Bayern-Augsburg-Klostergasse 20a", - "timezone": "Europe/Berlin", - "quota": { - "JTS1_ENERGY_STREAM_REPORT": { - "bpSoc": 88, - "mpptPwr": 0, - "bpPwr": -836, - "updateTime": "2025-05-03 03:15:22", - "sysLoadPwr": 823, - "sysGridPwr": -13, - "pv1Pwr": 0, - "pvInvPwr": 0, - "pv2Pwr": 0 - }, - "JTS1_EMS_CHANGE_REPORT": { - "sys14aEnable": false, - "afciFaultMaxValueCh1": 0, - "pcsHighVolOnGrid": 253, - "bpChgDsgSta": 0, - "pcsLowVolTime2": 300, - "duration": 0, - "pcsOngridReconnectFlag": 0, - "emsSgReadyEn": false, - "pcsLowFreqTime1": 100, - "wireless4gIccid": "", - "pcsSendEnd": 0, - "sysBatDsgDownLimit": 15, - "bpOnlineSum": 2, - "emsWorkState": 0, - "pcsOverVolDeratingSwitch": 0, - "emsSgRunStat": 0, - "mppt1WarningCode": 0, - "pcsAntiBackFlowImportCurrentLimit": 0, - "pcsAntiBackFlowSwitch": 1, - "pcsOverFreqDeratingStartDelay": 0, - "pcsActivePowerDeratingPercent": 1, - "mppt1FaultCode": 2, - "pcs10minOverVolTime": 100, - "pcsOverVolTime1": 100, - "pcsAvgOvpProtectValue": 0, - "pcsLowVolOnGrid": 195.5, - "afciFaultValueCh1": 0, - "ethWanStat": 0, - "pcsCospPf4": 0, - "pcsAcWarningCode": 0, - "pcs10minOverVol": 253, - "bpLineOffFlag": 0, - "pcsUnderFreqIncrementRecoverSlope": 0.09, - "pcsReactPwrModeSelect": 0, - "pcsPowerDeratingSet": 200, - "pcsLowVolTime3": 240, - "afciProtectValueCh2": 0, - "devMaxPower": 0, - "sysRateCtrlTime": 60, - "pcsUnderFreqIncrementStart": 49.8, - "afciFaultValueCh2": 0, - "pcsLowVolRideThroughStart1": 184, - "chgDsgMode": 0, - "iot4gSta": 2, - "pcsOfpProtectCnt": 0, - "pcsCospP1": 0.1, - "pcsFaultRecoverOnGridWaitTime": 60000, - "batRelayCloseFailFlag": 0, - "mppt2FaultCode": 2, - "pcsLowVol2": 103.5, - "endTimestamp": 0, - "afciFaultFlagCh2": 0, - "pcsFunctionEnable": 0, - "pcsOverVolDeratingTimeConst": 10, - "pcsLowVolRideThroughRecover": 195.5, - "pcsLowFreqTime2": 100, - "pcsLowVolRideThroughProtectTime3": 1000, - "pcsLowVolRideThroughProtectTime2": 3000, - "sysCalStat": 0, - "pcsReactPwrPercent": 0, - "pcsLowVolRideThroughProtectTime1": 5200, - "pcsOverVolRideThroughStart1": 265.65, - "pcsAutoTestPercent": 0, - "pcsRelayStateShow": 13732943, - "pcsOverVolDeratingStart": 253, - "pcsQuQ3": 0, - "pcsUvp2ProtectValue": 0, - "pcsFaultRecoverHighFreqOnGrid": 50.1, - "pcsPowerDeratingFlag": 5, - "pcsOverFreqDeratingCutoffPower": 0, - "pcsActivePowerSoftstartSwitch": 1, - "pcsCospPf2": -1, - "wifiStaStat": 0, - "pcsFastCheck": 0, - "pcsQuLockinPower": 0, - "sysMulPeakSwitch": false, - "pcsUvp1ProtectCnt": 0, - "pcsOverFreqDeratingRecoverSlopeSwitch": 1, - "pcsFaultRecoverLowFreqOnGrid": 47.53, - "pcsCospPf1": -1, - "iot4gErr": 7, - "pcsCospP3": 1, - "iot4gOn": 1, - "pcsOverVolTime2": 100, - "emsFeedPwr": 0, - "afciEnSet": 0, - "pcsOvpProtectCnt": 0, - "pcsCospP4": 0, - "bpReverseFlag": 0, - "pcsActivePowerSoftstartTime": 666, - "afciSelfTestCmdState": 0, - "afciProtectValueCh1": 0, - "parallelTypeCur": 0, - "emsFeedRatio": 100, - "emsCtrlLedBright": 10, - "userRole": 0, - "pcsOverFreqTime2": 100, - "pcsOverVolDeratingStartingPower": 1, - "afciFaultClearState": 0, - "pcsFaultRecoverHighVolOnGrid": 253, - "pcsUfpProtectCnt": 0, - "sysStateBit": 0, - "pcsOverVolTime3": 0, - "pcsRunSta": "RUNSTA_RUN", - "virtualHardEdition": 1, - "afciSwitchFreqCh1": 0, - "emsFeedMode": 1, - "batRealyStatus": 2, - "pcsQuQ1": 0.6, - "pcsReconnectGridDetectSwitch": 1, - "pcsUnderFreqIncrementRecoverSlopeSwitch": 1, - "pcsRunFsmState": 8669282, - "pcsHvrtLvrtSwitch": 1, - "emsBackupEvent": 0, - "pcsCospP2": 0.5, - "pcsOfpProtectValue": 0, - "sysOnOffMachineStat": 0, - "pcsFaultRecoverLowVolOnGrid": 195.5, - "pcsOverFreqDeratingRecoverSlope": 0.09, - "parallelTypeSet": 0, - "afciEnableCmdState": 0, - "pcsLowVolRideThroughStart2": 103.5, - "afciIsExist": 0, - "afciEn": 0, - "pcsUnderFreqIncrementEnd": 49.8, - "pcsOverFreqDeratingFrozeSwitch": 0, - "bpTotalDsgEnergy": 1368561, - "bpTotalChgEnergy": 1419246, - "sys14aType": 1, - "rateCtrlSwtich": false, - "pcsActivePowerNormalRampUpRate": 60, - "pcsOverVolDeratingEnd": 257.6, - "emsStopAll": 0, - "updateTime": "2025-05-03 03:17:49", - "bpSoc": 87, - "pcsOnGridWaitTime": 60000, - "pcsActivePowerDeratingSwitch": 0, - "pcsReactPwrCompensation": 0.0062, - "pcsOverVolRideThroughProtectTime2": 1000, - "pcsQuMinimumCosphi": 0.4, - "pcsPcsAntiBackFlowProtectSwitch": 0, - "pcsVolRecoverTime": 1000, - "pcsAutoTestState": 0, - "pcsOverFreq1": 51.5, - "parallelType": 0, - "pcsActivePowerSoftStartRate": 0.1, - "pcsLowVolTime1": 3000, - "pcsOverVolDeratingEndPower": 0, - "parallelAllowState": false, - "sysHeatStat": 0, - "bpRestartFlag": 1, - "pcsOverVol2": 287.5, - "pcsUnderFreqIncrementSlope": 0.4, - "pcsAcErrCode": 0, - "pcsOverFreqDeratingEndDelay": 0, - "pcsLowVolRecover": 195.5, - "pcsFreqRecoverTime": 1000, - "pcs10minOverVolSwitch": 1, - "sysMulPeakTime": 1200, - "pcsOverVolDeratingDaleyTime": 0, - "pcsOverFreqTime1": 100, - "afciFaultCntCh1": 0, - "pcsUvp1ProtectValue": 0, - "pcsOverVolRideThroughProtectTime1": 5500, - "pcsHighVolRideThroughRecover": 253, - "pcsOverFreq2": 51.5, - "pcsQuV2": 223.1, - "pcsUnderFreqIncrementFrozeSwitch": 0, - "pcsFreqLocalCommand": 1, - "pcsHighFreqOnGrid": 50.1, - "pcsUvp2ProtectCnt": 0, - "pcsDcErrCode": 0, - "pcsAntiBackFlowExportCurrentLimit": 0, - "pcsOverVol1": 287.5, - "sysTypeCfg": 0, - "pcsOverVol3": 0, - "pcsPfValue": 1, - "sysMeterCfg": 0, - "emsWordMode": "WORKMODE_SELFUSE", - "pcsOverFreqDeratingPowerBased": 2, - "relay14a": 0, - "pcsLowVol1": 184, - "emsCtrlLedType": 1, - "afciFaultCntCh2": 0, - "pcsOverVolRecover": 253, - "afciFaultFlagCh1": 0, - "pcsIslandDetectSwitch": 1, - "pcsActivePowerGradient": 0.0033, - "pcsOverVolRideThroughStart2": 287.5, - "pcsOverFreqRecover": 50.1, - "pcsQuQ2": 0, - "afciSwitchFreqCh2": 0, - "pcsCospPf3": -0.9, - "pcsQuTimeConst": 10, - "pcsQuV4": 246.1, - "pcsLowFreq2": 47.5, - "pcsLowVol3": 57.5, - "pcsLowFreqRecover": 47.53, - "pcsOvpProtectValue": 0, - "pcsLowFreq1": 47.5, - "pcsQuV1": 213.90001, - "pcsAutoTestFlag": 0, - "pcsOverFreqDeratingEnd": 50.2, - "pcsQuQ4": -0.6, - "pcsOverFreqDeratingSlope": 0.4, - "pcsFreqExternalSignal": 0, - "chgDsgPwr": 700, - "pcsOverFreqDeratingSwitch": 0, - "evBindList": { - "evSn": [ - "AC31ZEH4AG130052" - ] - }, - "pcsUfpProtectValue": 0, - "pcsLowFreqOnGrid": 47.53, - "mppt2WarningCode": 0, - "pcsUnderFreqIncrementEndDelay": 0, - "afciFaultMaxValueCh2": 0, - "pcsUnderFreqIncrementSwitch": 0, - "pcsQuV3": 236.9, - "pcsQuLockoutPower": 0, - "afciSellfTestResult": 0, - "iot4gPdp": -1, - "pcsLowVolRideThroughStart3": 34.5, - "pcsSafetyCountryCodeSelection": 4, - "pcsRelaySelfCheckSta": 0, - "pcsAvgOvpProtectCnt": 0, - "sysGridSta": 0, - "batSoftRelayStatus": 0, - "pcsUnderFreqIncrementStartDelay": 0, - "pcsOverFreqDeratingStart": 50.2 - }, - "JTS1_BP_STA_REPORT": { - "HJ3AZDH5ZG3G0490": "{\n \"bpPwr\": -0.8074197,\n \"bpSoc\": 88,\n \"bpSoh\": 100,\n \"bpTemp\": [29.0, 30.0, 29.0, 30.0, 30.0, 29.0, 29.0, 29.0, 29.0],\n \"bpCellMaxVol\": 3319.0,\n \"bpCellMinVol\": 3318.0,\n \"bpRunSta\": \"RUNSTA_RUN\",\n \"bpVol\": 53.066,\n \"bpAmp\": -0.015215387,\n \"bpBusVol\": 799.216,\n \"bpErrCode\": 0,\n \"bpCellVol\": [3319.0, 3319.0, 3318.0, 3318.0, 3318.0, 3319.0, 3318.0, 3318.0, 3318.0, 3318.0, 3318.0, 3318.0, 3318.0, 3319.0, 3318.0, 3318.0],\n \"bpDsrc\": 1,\n \"bpSn\": \"SEozQVpESDVaRzNHMDQ5MA==\",\n \"bpCycles\": 155,\n \"bpBalanceState\": 0,\n \"bpHvMosTemp\": 40.0,\n \"bpLvMosTemp\": 38.0,\n \"bpPtcTemp\": 29.0,\n \"bpHtsTemp\": 34.0,\n \"bpBusNegTemp\": 35.0,\n \"bpBusPosTemp\": 36.0,\n \"bpEnvTemp\": 35.0,\n \"bpAccuChgCap\": 13263555,\n \"bpAccuDsgCap\": 13246290,\n \"bpDesignCap\": 100000,\n \"bpFullCap\": 100000,\n \"bpMaxCellTemp\": 30.0,\n \"bpMinCellTemp\": 29.0,\n \"bpMaxMosTemp\": 40.0,\n \"bpMinMosTemp\": 38.0,\n \"bpBmsFault\": 0,\n \"bpEcloundSoc\": 65535,\n \"bpHeartbeatVer\": 33,\n \"bpTimestamp\": 1746213449,\n \"bpRealSoc\": ...", - "updateTime": "2025-05-03 03:17:39", - "HJ3AZDH5ZG3G0384": "{\n \"bpPwr\": -841.428,\n \"bpSoc\": 88,\n \"bpSoh\": 100,\n \"bpTemp\": [30.0, 31.0, 31.0, 31.0, 31.0, 30.0, 30.0, 31.0, 31.0],\n \"bpCellMaxVol\": 3315.0,\n \"bpCellMinVol\": 3313.0,\n \"bpRunSta\": \"RUNSTA_RUN\",\n \"bpVol\": 53.005,\n \"bpAmp\": -15.874502,\n \"bpBusVol\": 798.7922,\n \"bpErrCode\": 0,\n \"bpCellVol\": [3315.0, 3314.0, 3314.0, 3314.0, 3314.0, 3313.0, 3313.0, 3314.0, 3313.0, 3314.0, 3314.0, 3314.0, 3314.0, 3314.0, 3314.0, 3314.0],\n \"bpDsrc\": 2,\n \"bpSn\": \"SEozQVpESDVaRzNHMDM4NA==\",\n \"bpCycles\": 160,\n \"bpBalanceState\": 0,\n \"bpHvMosTemp\": 38.0,\n \"bpLvMosTemp\": 37.0,\n \"bpPtcTemp\": 31.0,\n \"bpHtsTemp\": 35.0,\n \"bpBusNegTemp\": 35.0,\n \"bpBusPosTemp\": 36.0,\n \"bpEnvTemp\": 35.0,\n \"bpAccuChgCap\": 13259968,\n \"bpAccuDsgCap\": 13221698,\n \"bpDesignCap\": 100000,\n \"bpFullCap\": 100000,\n \"bpMaxCellTemp\": 31.0,\n \"bpMinCellTemp\": 30.0,\n \"bpMaxMosTemp\": 38.0,\n \"bpMinMosTemp\": 37.0,\n \"bpBmsFault\": 0,\n \"bpEcloundSoc\": 65535,\n \"bpHeartbeatVer\": 33,\n \"bpTimestamp\": 1746213455,\n \"bpRealSoc\": 88....", - "": "{\n \"bpTemp\": [],\n \"bpCellVol\": []\n}" - }, - "JTS1_LOGY_DEV_REPORT": { - "HPReport": { - "devSn": "", - "online": 0, - "errorCode": "" - }, - "updateTime": "2025-05-03 03:12:27" - }, - "JTS1_EMS_PARAM_CHANGE_REPORT": { - "smartCtrl": false, - "updateTime": "2025-05-03 03:12:27", - "breakerCapacityMax": 32, - "energyEfficientEnable": false, - "breakerEnableState": true, - "bpBurst": false, - "lowerPowerStat": false, - "sysZone": 0, - "sysTimeTab": 0, - "devSoc": 80 - }, - "JTS1_ERROR_CODE_MASK_REPORT": { - "errorCode": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "updateTime": "2025-05-03 02:01:35" - }, - "JTS1_EDEV_BIND_LIST_REPORT": { - "213": "{\n \"devAddr\": 213,\n \"devItem\": []\n}", - "214": "{\n \"devAddr\": 214,\n \"devItem\": []\n}", - "updateTime": "2025-05-03 02:01:37" - }, - "JTS1_EMS_ALL_TIMER_TASK_REPORT": { - "timeTaskCfg": [], - "updateTime": "2025-05-02 15:34:18" - }, - "JT303_EDEV_SYS_REPORT": { - "devLastInfo": 31, - "devInfoFix": { - "devSubInfoFix": [ - { - "devAttrCur": 197424, - "bindType": 1, - "devInfo": { - "devAddr": 209, - "devSn": "AC31ZEH4AG130052", - "subAddr": 1 - }, - "devFlag": 131175, - "errorCode": 0, - "onlineBits": 2, - "modbusAddr": 52, - "warnCode": 0, - "memDevIdx": 0, - "targetMax": 11000, - "faultCode": 0, - "sceneType": 3, - "currentPrio": 1, - "devStateInfo": 5 - } - ] - }, - "freeNum": 35, - "devInfoVar": { - "devSubInfoVar": [ - { - "allocatedPower": 0, - "realPowerLock": 0, - "refPower": 0, - "devInfo": { - "devAddr": 209, - "devSn": "AC31ZEH4AG130052", - "subAddr": 1 - } - } - ] - }, - "updateTime": "2025-05-03 03:11:48", - "usedNum": 1, - "sysFlag": 35, - "devLastMinInfo": 31, - "socDev": 80, - "dispatchType": 2, - "socCur": 88.93504, - "stratType": 1, - "solarFlag": 985, - "devFirstInfo": 32, - "startState": 9, - "pclPwrBase": -851.92706, - "feedPwrCap": 12000 - }, - "JTS1_EMS_HEARTBEAT": { - "emsBusVoltRipple": 1.241394, - "pcsInterruptOccupancyRate": 88, - "bpRemainWatth": 9355, - "emsStartFsmState": 1078644260, - "emsAcMakeupMinSoc": 0, - "pcsActivePowerLimitDn": -12000, - "emsMpptStartupState": 1, - "pcsAverageVoltage": 1.0132734, - "updateTime": "2025-05-03 03:18:33", - "pcsActivePowerRef": 663.75104, - "pcsAPhase": { - "vol": 232.77829, - "amp": 1.2337433, - "actPwr": -220.07922, - "reactPwr": 184.506, - "apparentPwr": 287.18866 - }, - "pcsRelayStateShow": 13732943, - "emsMpptRunState": 0, - "pcsBpPower": -816.97534, - "pcsVbusRef": 770, - "pcsVgridThd": 0.0016662398, - "pcsLoadInfo": [ - { - "vol": 232.77829, - "amp": 0, - "freq": 49.990864, - "pwr": 0 - }, - { - "vol": 232.9028, - "amp": 0, - "freq": 49.99184, - "pwr": 0 - }, - { - "vol": 232.37206, - "amp": 0, - "freq": 49.99131, - "pwr": 0 - } - ], - "emsBpSelfcheckState": 1, - "meterHeartBeat": [ - { - "meterAddr": 106, - "meterType": 15, - "meterData": [ - -135.6, - 234.4, - -110.5, - -11.700012 - ] - } - ], - "pcsActPwr": -656.1483, - "emsBpStartupState": 1, - "pcsDci": 0.0012805257, - "emsBpPower": -813.38104, - "emsBusVoltErrSlidFilter": 0.66655725, - "emsLpStateFlag": 0, - "pcsBpPowerChgLimit": 4999.1465, - "emsBpChgRequest": 0, - "pcsAcFreq": 49.99, - "pcsGridSafetyFuncRecord": 234905714, - "emsMpptSelfcheckState": 1, - "pcsBusVolt": 797.13464, - "emsPvInvPwr": 0, - "pcsActivePowerLimitUp": 663.75104, - "mpptHeartBeat": [ - { - "mpptPv": [ - { - "vol": 42.0932, - "amp": 0.05277126, - "lightSta": true, - "pwr": 2.221311 - }, - { - "vol": 41.54573, - "amp": 0.08966458, - "lightSta": true, - "pwr": 3.7251804 - } - ], - "mpptTempVal": [ - 41.2, - 43.5, - 43.1 - ], - "mpptInsResist": 0 - } - ], - "emsBpChg": 5000.0005, - "emsMpptHbState": 17, - "emsLpMpptCnt": 0, - "pcsMeterPower": -13.199997, - "emsAcMakeupCnt": 0, - "emsBpDsg": -6600, - "emsLpState": 0, - "emsLpType": 0, - "emsNtcTempMax": 43.5, - "emsAcMakeupTriggleSoc": 17, - "emsLpBpCnt": 0, - "pcsBPhase": { - "vol": 232.9028, - "amp": 1.2401944, - "actPwr": -218.97543, - "reactPwr": 188.36415, - "apparentPwr": 288.84476 - }, - "pcsGridSafetyStateRecord": 9486428, - "emsMpptModStat": 1, - "pcsReactivePowerRef": 0, - "pcsDcv": 0, - "emsBpAliveNum": 2, - "emsAcMakeupExitSoc": 19, - "emsBusVolt": 800.071, - "emsActiveOffGridCmd": 0, - "emsSocCalibState": 0, - "pcsGridInvErrorRms": 6.710472, - "emsSysCfg": 7, - "bpDsgTime": 672, - "pcsLeakAmp": -6.4453125, - "pcsCommInterfaceState": 17120, - "emsSocCalibRequest": 0, - "emsPcsSelfcheckState": 1, - "pcsCPhase": { - "vol": 232.37206, - "amp": 1.2358344, - "actPwr": -217.09364, - "reactPwr": 187.98643, - "apparentPwr": 287.17337 - }, - "emsPcsStartupState": 1 - }, - "JTS1_EMS_PV_INV_ENERGY_STREAM_REPORT": { - "pvInvPwr": 0, - "updateTime": "2025-05-03 03:15:22" - }, - "JT303_EDEV_PRIORITY_LIST_REPORT": { - "devPrioReport": [ - { - "devPrio": 1, - "data": "Aw==", - "enable": true, - "devInfo": { - "devAddr": 209, - "devSn": "AC31ZEH4AG130052", - "subAddr": 1 - } - } - ], - "updateTime": "2025-05-03 02:11:24" - }, - "JTS1_EVCHARGING_REPORT": { - "evSn": "AC31ZEH4AG130052", - "workMode": 2, - "useGridFirst": 1, - "evOnoffSet": 0, - "errorCode": "AAAAAAAAAAA=", - "evPhaseSet": 1, - "orderState": 2, - "orderTime": 29670, - "orderEndTimestamp": 1746213532, - "defaultVehicleId": "534", - "chargingStatus": "EV_CHG_STS_SUSPENDED_EVSE", - "expectChargingEnergy": 0, - "evPwr": 0, - "switchBits": 0, - "evDetectPhase": 3, - "orderStartTimestamp": 1746183862, - "onlineBits": 2, - "updateTime": "2025-05-03 03:18:53", - "evUserManual": 0, - "evChargingEnergy": 17256, - "evCurrSet": 0, - "chargeVehicleId": "534", - "evPlugAndPlay": 1, - "evExeMode": 2, - "stopChargingSoc": 255 - }, - "JTS1_ERROR_CHANGE_REPORT": { - "bpErrCode": [ - { - "errCode": [], - "moduleSn": "SEozQVpESDVaRzNHMDQ5MA==" - }, - { - "errCode": [], - "moduleSn": "SEozQVpESDVaRzNHMDM4NA==" - } - ], - "emsErrCode": { - "errCode": [], - "moduleSn": "SEozN1pESDVaRzVXMDEwOQ==" - }, - "pcsErrCode": { - "errCode": [], - "moduleSn": "SEozMTIxMDJCRzRKMTMzMQ==" - }, - "updateTime": "2025-05-03 03:12:28" - }, - "JTS1_ECOLOGY_DEV_BIND_LIST_REPORT": { - "devItem": [], - "updateTime": "2025-05-03 02:01:36" - }, - "JTS1_EV_CHARGING_ENERGY_STREAM_REPORT": { - "updateTime": "2025-05-03 03:15:24", - "evStreamShow": [ - { - "evSn": "AC31ZEH4AG130052", - "evPwr": 0 - } - ] - }, - "JTS1_EV_CHARGING_TIMER_TASK_REPORT": { - "timeTaskCfg": [ - { - "taskIndex": 1, - "timeParam": 0, - "param": 16, - "isCfg": 0, - "isEffect": false, - "timeMode": 65, - "timeTable": [ - 19660800 - ], - "type": 1, - "isEnable": false - } - ], - "evSn": "AC31ZEH4AG130052", - "updateTime": "2025-05-03 02:01:36" - } - } - }, - "eagleEyeTraceId": "ea1a2a582917462135513308071d0007", - "tid": "" - } \ No newline at end of file diff --git a/powerocean_powerpulse/sensor.py b/powerocean_powerpulse/sensor.py deleted file mode 100644 index 8152d8f..0000000 --- a/powerocean_powerpulse/sensor.py +++ /dev/null @@ -1,375 +0,0 @@ -from datetime import timedelta -from collections import defaultdict - -from homeassistant.components.sensor import SensorEntity -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.sensor import SensorStateClass -from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers import entity_registry -from homeassistant.exceptions import IntegrationError - -from .const import ( - DOMAIN, - _LOGGER, - ATTR_PRODUCT_DESCRIPTION, - ATTR_DESTINATION_NAME, - ATTR_SOURCE_NAME, - ATTR_UNIQUE_ID, - ATTR_PRODUCT_SERIAL, - ATTR_PRODUCT_NAME, - ATTR_PRODUCT_VENDOR, - ATTR_PRODUCT_BUILD, - ATTR_PRODUCT_VERSION, - ATTR_PRODUCT_FEATURES, - ISSUE_URL_ERROR_MESSAGE, -) - -from .ecoflow import Ecoflow, AuthenticationFailed - - -# Setting up the adding and updating of sensor entities -async def async_setup_entry(hass, config_entry, async_add_entities): - # Retrieve the API instance from the config_entry data - ecoflow = hass.data[DOMAIN][config_entry.entry_id] - device_id = ecoflow.device["serial"] - - # Call EcoFlow to get access to the API data - try: - auth_check = await hass.async_add_executor_job(ecoflow.authorize) - - if not auth_check: - # If device returns False or is empty, log an error and return - _LOGGER.warning( - f"{device_id}: It appears the PowerOcean device is offline or has changed host." - + ISSUE_URL_ERROR_MESSAGE - ) - - except AuthenticationFailed as error: - _LOGGER.warning(f"{device_id}: Authentication failed: {error}") - return - - try: - # Fetch the sensor data from the device - data = await hass.async_add_executor_job(ecoflow.fetch_data) - - if not data: - # If data returns False or is empty, log an error and return - _LOGGER.warning( - f"{device_id}: Failed to fetch sensor data => 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 52067b9..0000000 Binary files a/voice_satellite/models/hey_baby.tflite and /dev/null differ