-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Robert Schindler
committed
Jan 7, 2020
1 parent
ce4e709
commit 003f25f
Showing
10 changed files
with
339 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
WORK IN PROGRESS | ||
|
||
# Here you configure the attributes of the entity to be controlled by the schedule. | ||
attributes: | ||
# The attribute to be controlled, this could be e.g. "state" or "brightness". | ||
# A value of null creates a write-only attribute. This has to be used when you want | ||
# to control a property whose current value is not reflected in any of the entity's | ||
# attributes. Don't do this if not really necessary, since doing so means that | ||
# Schedy won't be able to verify that the value has been transmitted correctly. If | ||
# you must use a write-only attribute, you may also want to set send_retries to a | ||
# low value in order to avoid excessive network load. | ||
- attribute: first | ||
- attribute: second | ||
- ... | ||
|
||
# Here you configure the values you want to be able to return from your schedule. | ||
values: | ||
# Each value is a list of the values for the individual attributes configured above. | ||
# Schedy compares the entity's current attributes against the values defined here | ||
# in order to find the value currently active. | ||
# The special attribute value "*" is a wildcard and will, when used, match any | ||
# value of that particular attribute. | ||
# Additionally, you don't have to include all attributes in every single value, | ||
# only the first N attributes which values are provided for are compared against | ||
# the entity's state for the value to match. | ||
- value: ["value of 1st attribute", "value of 2nd attribute", ...] | ||
# The services that have to be called in order to make the actor report this value. | ||
calls: | ||
# Which service to call | ||
- service: ... | ||
# Optionally, provide service data. | ||
data: | ||
# Set to false if you don't want the entity_id field to be included in service data. | ||
#include_entity_id: true | ||
# More values# | ||
- ... | ||
|
||
# Set this to true if you want Schedy to treat string attributes of an entity the | ||
# same, no matter if they're reported in lower or upper case. This is handy for some | ||
# MQTT devices, for instance, which sometimes report a state of "ON", while others say | ||
# "on". | ||
#ignore_case: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
Generic Actor Version 2 | ||
======================= | ||
|
||
The ``generic2`` actor can be used for controlling different types of entities like | ||
numbers or media players, even those having multiple adjustable attributes such as | ||
roller shutters with tilt and position. | ||
|
||
It works by defining a set of values and, for each of these values, what services | ||
have to be called in order to reach the state represented by that value. | ||
|
||
Instead of a single value such as ``"on"`` or ``"off"``, you may also generate a | ||
tuple of multiple values like ``(50, 75)`` or ``("on", 10)`` in your schedule rules, | ||
where each slot in that tuple corresponds to a different attribute of the entity. | ||
|
||
If you want to see how this actor type can be used, have a look at the | ||
:doc:`../switch/index`. | ||
|
||
|
||
Configuration | ||
------------- | ||
|
||
.. include:: ../config.rst.inc | ||
|
||
|
||
Supported Values | ||
---------------- | ||
|
||
Every value that has been configured in the ``values`` section of the actor | ||
configuration may be returned from a schedule. | ||
|
||
Examples: | ||
|
||
:: | ||
|
||
- v: "on" | ||
- x: "-40 if is_on(...) else Next()" | ||
|
||
As soon as you configure multiple slots (attributes to be controlled), a list or | ||
tuple with a value for each attribute is expected. The order is the same in which | ||
the slots were specified in the configuration. | ||
|
||
Examples: | ||
|
||
:: | ||
|
||
- v: ['on', 20] | ||
- x: "(-40, 'something') if is_on(...) else Next()" | ||
|
||
.. note:: | ||
|
||
When specifying the values ``on`` and ``off``, enclose them in quotes | ||
as shown above to inform the YAML parser you don't mean the booleans | ||
``True`` and ``False`` instead. |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
actor_type: generic2 | ||
actor_templates: | ||
default: | ||
attributes: | ||
- attribute: state | ||
values: | ||
- value: ["on"] | ||
calls: | ||
- service: homeassistant.turn_on | ||
- value: ["off"] | ||
calls: | ||
- service: homeassistant.turn_off | ||
ignore_case: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
""" | ||
This module implements the generic actor. | ||
""" | ||
|
||
import typing as T | ||
|
||
import copy | ||
|
||
import voluptuous as vol | ||
|
||
from ... import common | ||
from .base import ActorBase | ||
|
||
|
||
ALLOWED_VALUE_TYPES = (bool, float, int, str, type(None)) | ||
ALLOWED_VALUE_TYPES_T = T.Union[ # pylint: disable=invalid-name | ||
bool, float, int, str, None | ||
] | ||
WILDCARD_ATTRIBUTE_VALUE = "*" | ||
|
||
|
||
class Generic2Actor(ActorBase): | ||
"""A configurable, generic actor for Schedy that can control multiple | ||
attributes at once.""" | ||
|
||
name = "generic2" | ||
config_schema_dict = { | ||
**ActorBase.config_schema_dict, | ||
vol.Optional("attributes", default=None): vol.All( | ||
vol.DefaultTo(list), | ||
[ | ||
vol.All( | ||
vol.DefaultTo(dict), | ||
{vol.Optional("attribute", default=None): vol.Any(str, None)}, | ||
) | ||
], | ||
), | ||
vol.Optional("values", default=None): vol.All( | ||
vol.DefaultTo(list), | ||
[ | ||
vol.All( | ||
vol.DefaultTo(dict), | ||
{ | ||
vol.Required("value"): vol.All( | ||
[vol.Any(*ALLOWED_VALUE_TYPES)], vol.Coerce(tuple), | ||
), | ||
vol.Optional("calls", default=None): vol.All( | ||
vol.DefaultTo(list), | ||
[ | ||
vol.All( | ||
vol.DefaultTo(dict), | ||
{ | ||
vol.Required("service"): str, | ||
vol.Optional("data", default=None): vol.All( | ||
vol.DefaultTo(dict), dict | ||
), | ||
vol.Optional( | ||
"include_entity_id", default=True | ||
): bool, | ||
}, | ||
) | ||
], | ||
), | ||
}, | ||
) | ||
], | ||
# Sort by number of attributes (descending) for longest prefix matching | ||
lambda v: sorted(v, key=lambda k: -len(k["value"])), | ||
), | ||
vol.Optional("ignore_case", default=False): bool, | ||
} | ||
|
||
def _find_value_cfg(self, value: T.Tuple) -> T.Any: | ||
"""Returns the config matching given value or ValueError if none found.""" | ||
for value_cfg in self.cfg["values"]: | ||
_value = value_cfg["value"] | ||
if len(_value) != len(value): | ||
continue | ||
for idx, attr_value in enumerate(_value): | ||
if attr_value not in (WILDCARD_ATTRIBUTE_VALUE, value[idx]): | ||
break | ||
else: | ||
return value_cfg | ||
raise ValueError("No configuration for value {!r}".format(value)) | ||
|
||
def _populate_service_data(self, data: T.Dict, fmt: T.Dict[str, T.Any]) -> None: | ||
"""Fills in placeholders in the service data definition.""" | ||
memo = {data} # type: T.Set[T.Union[T.Dict, T.List]] | ||
while memo: | ||
obj = memo.pop() | ||
if isinstance(obj, dict): | ||
_iter = obj.items() # type: T.Iterable[T.Tuple[T.Any, T.Any]] | ||
elif isinstance(obj, list): | ||
_iter = enumerate(obj) | ||
else: | ||
continue | ||
for key, value in _iter: | ||
if isinstance(value, str): | ||
try: | ||
formatted = value.format(fmt) | ||
# Convert int/float values to appropriate type | ||
try: | ||
_float = float(formatted) | ||
_int = int(formatted) | ||
except ValueError: | ||
# It's a string value | ||
obj[key] = formatted | ||
else: | ||
# It's an int or float | ||
obj[key] = _int if _float == _int else _float | ||
except (IndexError, KeyError, ValueError) as err: | ||
self.log( | ||
"Couldn't format service data {!r} with values " | ||
"{!r}: {!r}, omitting data.".format(value, fmt, err), | ||
level="ERROR", | ||
) | ||
elif isinstance(value, (dict, list)): | ||
memo.add(value) | ||
|
||
def do_send(self) -> None: | ||
"""Executes the configured services for self._wanted_value.""" | ||
value = self._wanted_value | ||
# Build formatting data with values of all attributes | ||
fmt = {"entity_id": self.entity_id} | ||
for idx in range(len(self.cfg["attributes"])): | ||
fmt["attr{}".format(idx + 1)] = value[idx] if idx < len(value) else None | ||
|
||
for call_cfg in self._find_value_cfg(value)["calls"]: | ||
service = call_cfg["service"] | ||
data = copy.deepcopy(call_cfg["data"]) | ||
self._populate_service_data(data, fmt) | ||
if call_cfg["include_entity_id"]: | ||
data.setdefault("entity_id", self.entity_id) | ||
self.log( | ||
"Calling service {}, data = {}.".format(repr(service), repr(data)), | ||
level="DEBUG", | ||
prefix=common.LOG_PREFIX_OUTGOING, | ||
) | ||
self.app.call_service(service, **data) | ||
|
||
def filter_set_value(self, value: T.Tuple) -> T.Any: | ||
"""Checks whether the actor supports this value.""" | ||
if self.cfg["ignore_case"]: | ||
value = tuple(v.lower() if isinstance(v, str) else v for v in value) | ||
try: | ||
self._find_value_cfg(value) | ||
except ValueError: | ||
self.log( | ||
"Value {!r} is not known by this actor.".format(value), level="ERROR" | ||
) | ||
return None | ||
return value | ||
|
||
def notify_state_changed(self, attrs: dict) -> T.Any: | ||
"""Is called when the entity's state changes.""" | ||
items = [] | ||
for attr_cfg in self.cfg["attributes"]: | ||
attr = attr_cfg["attribute"] | ||
if attr is None: | ||
self.log("Ignoring state change (write-only attribute).", level="DEBUG") | ||
return None | ||
state = attrs.get(attr) | ||
self.log( | ||
"Attribute {!r} is {!r}.".format(attr, state), | ||
level="DEBUG", | ||
prefix=common.LOG_PREFIX_INCOMING, | ||
) | ||
if self.cfg["ignore_case"] and isinstance(state, str): | ||
state = state.lower() | ||
items.append(state) | ||
|
||
tpl = tuple(items) | ||
# Goes from len(tpl) down to 0 | ||
for size in range(len(tpl), -1, -1): | ||
value = tpl[:size] | ||
try: | ||
self._find_value_cfg(value) | ||
except ValueError: | ||
continue | ||
return value | ||
|
||
self.log( | ||
"Received state {!r} which is not configured as a value.".format(items), | ||
level="WARNING", | ||
) | ||
return None | ||
|
||
@staticmethod | ||
def validate_value(value: T.Any) -> T.Any: | ||
"""Converts lists to tuples.""" | ||
if isinstance(value, list): | ||
items = tuple(value) | ||
elif isinstance(value, tuple): | ||
items = value | ||
else: | ||
items = (value,) | ||
|
||
for index, item in enumerate(items): | ||
if not isinstance(item, ALLOWED_VALUE_TYPES): | ||
raise ValueError( | ||
"Value {!r} for {}. attribute must be of one of these types: " | ||
"{}".format(item, index + 1, ALLOWED_VALUE_TYPES) | ||
) | ||
return items |
Oops, something went wrong.