chore: untrack vendor code, remove powerocean leftover

- voice_satellite/ — Wake-Word ML-Modell (extern trainiert, von TaterTotterson),
  nicht eigener Config; via .gitignore ausgenommen.
- appdaemon/apps/controllerx/ — ControllerX v5.2.3 wird via HACS-AppDaemon
  gemanagt (xaviml/controllerx); via .gitignore ausgenommen.
  apps.yaml bleibt versioniert (eigene Mapping-Config).
- powerocean_powerpulse/ — alte Version (2025.01.07) der niltrip/powerocean
  Komponente, lag im falschen Pfad (/config/ statt custom_components/),
  wurde nie geladen. Aktive Version ist custom_components/powerocean_dev/
  (2026.05.01). Komplett entfernt.
This commit is contained in:
2026-05-02 14:31:07 +02:00
parent 5f01411780
commit a137bfc938
85 changed files with 0 additions and 9645 deletions
-39
View File
@@ -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 *
-150
View File
@@ -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"
@@ -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",
]
@@ -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())}"
)
@@ -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__}"
@@ -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})"
@@ -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)"
@@ -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})"
@@ -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})"
@@ -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"
)
@@ -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 {}
@@ -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)
@@ -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
@@ -1,4 +0,0 @@
class LightSupport:
EFFECT = 4
FLASH = 8
TRANSITION = 32
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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)
@@ -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)
)
@@ -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)
@@ -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)
@@ -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)
@@ -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()
@@ -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
@@ -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
@@ -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)
@@ -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
)
@@ -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
@@ -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,
}
@@ -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,
}
@@ -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
@@ -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,
}
@@ -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
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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']}"
@@ -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,
}
@@ -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,
}
@@ -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
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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,
}
@@ -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
@@ -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,
}
@@ -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
@@ -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,
}
@@ -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
-33
View File
@@ -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]
-1
View File
@@ -1 +0,0 @@
__version__ = "v5.2.3"
-86
View File
@@ -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)
@@ -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"
-189
View File
@@ -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
-38
View File
@@ -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}
----------------------------------------------------------------------------
"""
-72
View File
@@ -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"
]
}
-501
View File
@@ -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", "<no user 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."""
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

-12
View File
@@ -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"
}
-635
View File
@@ -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": ""
}
-375
View File
@@ -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
-31
View File
@@ -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%]"
}
}
}
@@ -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"
}
}
}
}
}
@@ -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"
}
}
}
}
}
-100
View File
@@ -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"
]
}
@@ -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"
]
}
-18
View File
@@ -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"
}
}
Binary file not shown.