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:
@@ -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 *
|
|
||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
__version__ = "v5.2.3"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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}
|
|
||||||
----------------------------------------------------------------------------
|
|
||||||
"""
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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 |
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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": ""
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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.
Reference in New Issue
Block a user