Skip to content

Commit

Permalink
Merge branch 'dev' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
xaviml committed Jul 7, 2021
2 parents cfc44d0 + 2778693 commit 44a471e
Show file tree
Hide file tree
Showing 88 changed files with 954 additions and 439 deletions.
2 changes: 1 addition & 1 deletion .cz.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.commitizen]
name = "cz_conventional_commits"
version = "4.13.0"
version = "4.14.0b1"
tag_format = "v$major.$minor.$patch$prerelease"
version_files = [
"apps/controllerx/cx_version.py",
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pytest-mock = "==3.6.1"
pytest-timeout = "==1.4.2"
mock = "==4.0.3"
pre-commit = "==2.13.0"
commitizen = "==2.17.11"
commitizen = "==2.17.12"
mypy = "==0.910"
flake8 = "==3.9.2"
isort = "==5.9.1"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ livingroom_controller:
class: E1810Controller
controller: sensor.livingroom_controller_action
integration: z2m
light: light.bedroom
light: light.livingroom
```
## Documentation
Expand Down
16 changes: 10 additions & 6 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ _This minor change does not contain any breaking changes._
_Note: Remember to restart the AppDaemon addon/server after updating to a new version._
PRERELEASE_NOTE

<!--
## :pencil2: Features
-->

## :hammer: Fixes

- Clean action handle when there is an error. This will help for error logging.
- Allow to pass parameters to predefined actions. You can check the parameters for each predefined action, and how to pass parameters in [here](https://xaviml.github.io/controllerx/advanced/predefined-actions). [ #78 ]
- Support for Light Group integration. To know how entity groups work, read [here](https://xaviml.github.io/controllerx/advanced/entity-groups) [ #330 ]
- Add option to read `unique_id` attribute for deCONZ integration. Read more about it [here](https://xaviml.github.io/controllerx/others/integrations#deconz) [ #333 ]

<!--
## :clock2: Performance
## :hammer: Fixes
-->

## :clock2: Performance

- Reduce calls to HA when entity is a group.

<!--
## :scroll: Docs
-->
Expand All @@ -26,6 +28,8 @@ PRERELEASE_NOTE
## :wrench: Refactor
-->

<!--
## :video_game: New devices
- [E1812](https://xaviml.github.io/controllerx/controllers/E1812) - add ZHA support [ #324 ]
-->
3 changes: 2 additions & 1 deletion apps/controllerx/cx_const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union

ActionFunction = Callable[..., Awaitable[Any]]
ActionFunctionWithParams = Tuple[ActionFunction, Tuple[Any, ...]]
ActionParams = Tuple[Any, ...]
ActionFunctionWithParams = Tuple[ActionFunction, ActionParams]
TypeAction = Union[ActionFunction, ActionFunctionWithParams]
ActionEvent = Union[str, int]
PredefinedActionsMapping = Dict[str, TypeAction]
Expand Down
89 changes: 74 additions & 15 deletions apps/controllerx/cx_core/action_type/predefined_action_type.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import inspect
from typing import Optional
from typing import Any, Dict, Optional, Tuple

from cx_const import ActionFunctionWithParams, PredefinedActionsMapping, TypeAction
from cx_const import (
ActionFunction,
ActionFunctionWithParams,
ActionParams,
PredefinedActionsMapping,
TypeAction,
)
from cx_core.action_type.base import ActionType
from cx_core.integration import EventData

Expand All @@ -13,21 +19,74 @@ def _get_action(action_value: TypeAction) -> ActionFunctionWithParams:
return (action_value, tuple())


def _get_arguments(
action: ActionFunction,
args: ActionParams,
predefined_action_kwargs: Dict[str, Any],
extra: Optional[EventData],
) -> 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():
attr_msg = f" {key}: {param.annotation.__name__}"
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):
action_key: str
predefined_action_key: str
predefined_action_kwargs: Dict[str, Any]
predefined_actions_mapping: PredefinedActionsMapping

def _raise_action_key_not_found(
self, action_key: str, predefined_actions: PredefinedActionsMapping
self, predefined_action_key: str, predefined_actions: PredefinedActionsMapping
) -> None:
raise ValueError(
f"`{action_key}` is not one of the predefined actions. "
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) -> None:
self.action_key = kwargs["action"]
self.predefined_action_key = kwargs.pop("action")
self.predefined_action_kwargs = kwargs
self.predefined_actions_mapping = (
self.controller.get_predefined_actions_mapping()
)
Expand All @@ -36,24 +95,24 @@ def initialize(self, **kwargs) -> None:
f"Cannot use predefined actions for `{self.controller.__class__.__name__}` class."
)
if (
not self.controller.contains_templating(self.action_key)
and self.action_key not in self.predefined_actions_mapping
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.action_key, self.predefined_actions_mapping
self.predefined_action_key, self.predefined_actions_mapping
)

async def run(self, extra: Optional[EventData] = None) -> None:
action_key = await self.controller.render_value(self.action_key)
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])
if "extra" in set(inspect.signature(action).parameters):
await action(*args, extra=extra)
else:
await action(*args)
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.action_key})"
return f"Predefined ({self.predefined_action_key})"
26 changes: 19 additions & 7 deletions apps/controllerx/cx_core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
Tuple,
TypeVar,
Union,
overload,
)

import appdaemon.utils as utils
import cx_version
from appdaemon.plugins.hass.hassapi import Hass # type: ignore
from appdaemon.plugins.mqtt.mqttapi import Mqtt # type: ignore
from appdaemon.plugins.hass.hassapi import Hass
from appdaemon.plugins.mqtt.mqttapi import Mqtt
from cx_const import (
ActionEvent,
ActionFunction,
Expand Down Expand Up @@ -85,6 +86,7 @@ class Controller(Hass, Mqtt):
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["Future[None]"]]
Expand Down Expand Up @@ -222,7 +224,15 @@ def get_default_actions_mapping(
raise ValueError(f"This controller does not support {integration.name}.")
return actions_mapping

def get_list(self, entities: Union[List[T], T]) -> List[T]:
@overload
def get_list(self, entities: List[T]) -> List[T]:
...

@overload
def get_list(self, entities: T) -> List[T]:
...

def get_list(self, entities):
if isinstance(entities, (list, tuple)):
return list(entities)
return [entities]
Expand Down Expand Up @@ -307,7 +317,7 @@ async def call_service(self, service: str, **attributes) -> Optional[Any]:
value = f"{value:.2f}"
to_log.append(f" - {attribute}: {value}")
self.log("\n".join(to_log), level="INFO", ascii_encode=False)
return await Hass.call_service(self, service, **attributes) # type: ignore
return await Hass.call_service(self, service, **attributes)

@utils.sync_wrapper
async def get_state(
Expand All @@ -319,7 +329,9 @@ async def get_state(
**kwargs,
) -> Optional[Any]:
rendered_entity_id = await self.render_value(entity_id)
return await super().get_state(rendered_entity_id, attribute, default, copy, **kwargs) # type: ignore
return await super().get_state(
rendered_entity_id, attribute, default, copy, **kwargs
)

async def handle_action(
self, action_key: str, extra: Optional[EventData] = None
Expand Down Expand Up @@ -395,15 +407,15 @@ async def call_action(
if delay > 0:
handle = self.action_delay_handles[action_key]
if handle is not None:
await self.cancel_timer(handle) # type: ignore
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
) # type: ignore
)
self.action_delay_handles[action_key] = new_handle
else:
await self.action_timer_callback({"action_key": action_key, "extra": extra})
Expand Down
7 changes: 2 additions & 5 deletions apps/controllerx/cx_core/feature_support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@

class FeatureSupport:

entity_id: str
controller: "TypeController"
update_supported_features: bool
_supported_features: Optional[int]

def __init__(
self,
entity_id: str,
controller: "TypeController",
supported_features: Optional[int] = None,
update_supported_features=False,
) -> None:
self.entity_id = entity_id
self.controller = controller
self._supported_features = supported_features
self.update_supported_features = update_supported_features
Expand All @@ -27,13 +24,13 @@ def __init__(
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(
self.entity_id, attribute="supported_features"
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.entity_id}`. Entity might not be available."
f"`supported_features` could not be read from `{self.controller.entity}`. Entity might not be available."
)
return self._supported_features

Expand Down
2 changes: 1 addition & 1 deletion apps/controllerx/cx_core/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _all_integration_subclasses(
subclasses = set(cls_.__subclasses__()).union(
[s for c in cls_.__subclasses__() for s in _all_integration_subclasses(c)]
)
return list(subclasses) # type: ignore
return list(subclasses)


def get_integrations(controller, kwargs) -> List[Integration]:
Expand Down
13 changes: 12 additions & 1 deletion apps/controllerx/cx_core/integration/deconz.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from cx_const import DefaultActionsMapping # type:ignore
from cx_core.integration import EventData, Integration

LISTENS_TO_ID = "id"
LISTENS_TO_UNIQUE_ID = "unique_id"


class DeCONZIntegration(Integration):
name = "deconz"
Expand All @@ -12,8 +15,16 @@ def get_default_actions_mapping(self) -> Optional[DefaultActionsMapping]:
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", id=controller_id
self.controller,
self.event_callback,
"deconz_event",
**{listens_to: controller_id}
)

async def event_callback(
Expand Down
8 changes: 5 additions & 3 deletions apps/controllerx/cx_core/integration/lutron_caseta.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

from appdaemon.plugins.hass.hassapi import Hass # type: ignore
from appdaemon.plugins.hass.hassapi import Hass
from cx_const import DefaultActionsMapping
from cx_core.integration import EventData, Integration

Expand All @@ -14,12 +14,14 @@ def get_default_actions_mapping(self) -> Optional[DefaultActionsMapping]:
async def listen_changes(self, controller_id: str) -> None:
await Hass.listen_event(
self.controller,
self.callback,
self.event_callback,
"lutron_caseta_button_event",
serial=controller_id,
)

async def callback(self, event_name: str, data: EventData, kwargs: dict) -> None:
async def event_callback(
self, event_name: str, data: EventData, kwargs: dict
) -> None:
button = data["button_number"]
action_type = data["action"]
action = f"button_{button}_{action_type}"
Expand Down
2 changes: 1 addition & 1 deletion apps/controllerx/cx_core/integration/mqtt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

from appdaemon.plugins.mqtt.mqttapi import Mqtt # type: ignore
from appdaemon.plugins.mqtt.mqttapi import Mqtt
from cx_const import DefaultActionsMapping
from cx_core.integration import EventData, Integration

Expand Down
2 changes: 1 addition & 1 deletion apps/controllerx/cx_core/integration/state.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

from appdaemon.plugins.hass.hassapi import Hass # type: ignore
from appdaemon.plugins.hass.hassapi import Hass
from cx_const import DefaultActionsMapping
from cx_core.integration import Integration

Expand Down
Loading

0 comments on commit 44a471e

Please sign in to comment.