Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions homeassistant/components/automation/numeric_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
_LOGGER = logging.getLogger(__name__)


async def async_attach_trigger(hass, config, action, automation_info):
async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="numeric_state"
):
"""Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
Expand Down Expand Up @@ -84,7 +86,7 @@ def call_action():
action(
{
"trigger": {
"platform": "numeric_state",
"platform": platform_type,
"entity_id": entity,
"below": below,
"above": above,
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/binary_sensor/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info):
state_automation.CONF_FROM: from_state,
state_automation.CONF_TO: to_state,
}
if "for" in config:
state_config["for"] = config["for"]
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]

return await state_automation.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
Expand All @@ -215,7 +215,7 @@ async def async_get_triggers(hass, device_id):
]

for entry in entries:
device_class = None
device_class = DEVICE_CLASS_NONE
state = hass.states.get(entry.entity_id)
if state:
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/device_automation/toggle_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ async def async_attach_trigger(
state.CONF_FROM: from_state,
state.CONF_TO: to_state,
}
if "for" in config:
state_config["for"] = config["for"]
if CONF_FOR in config:
state_config[CONF_FOR] = config[CONF_FOR]

return await state.async_attach_trigger(
hass, state_config, action, automation_info, platform_type="device"
Expand Down
145 changes: 145 additions & 0 deletions homeassistant/components/sensor/device_trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""Provides device triggers for sensors."""
import voluptuous as vol

import homeassistant.components.automation.numeric_state as numeric_state_automation
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
from homeassistant.const import (
ATTR_DEVICE_CLASS,
CONF_ABOVE,
CONF_BELOW,
CONF_ENTITY_ID,
CONF_FOR,
CONF_TYPE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
)
from homeassistant.helpers.entity_registry import async_entries_for_device
from homeassistant.helpers import config_validation as cv

from . import DOMAIN


# mypy: allow-untyped-defs, no-check-untyped-defs

DEVICE_CLASS_NONE = "none"

CONF_BATTERY_LEVEL = "battery_level"
CONF_HUMIDITY = "humidity"
CONF_ILLUMINANCE = "illuminance"
CONF_POWER = "power"
CONF_PRESSURE = "pressure"
CONF_SIGNAL_STRENGTH = "signal_strength"
CONF_TEMPERATURE = "temperature"
CONF_TIMESTAMP = "timestamp"
CONF_VALUE = "value"

ENTITY_TRIGGERS = {
DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}],
DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}],
DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}],
DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}],
DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}],
DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}],
DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}],
DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}],
DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}],
}


TRIGGER_SCHEMA = vol.All(
TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_TYPE): vol.In(
[
CONF_BATTERY_LEVEL,
CONF_HUMIDITY,
CONF_ILLUMINANCE,
CONF_POWER,
CONF_PRESSURE,
CONF_SIGNAL_STRENGTH,
CONF_TEMPERATURE,
CONF_TIMESTAMP,
CONF_VALUE,
]
),
vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)),
vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)),
vol.Optional(CONF_FOR): vol.Any(
vol.All(cv.time_period, cv.positive_timedelta),
cv.template,
cv.template_complex,
),
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
}
),
cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
)


async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
numeric_state_config = {
numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID],
numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE),
numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW),
numeric_state_automation.CONF_FOR: config.get(CONF_FOR),
}
if CONF_FOR in config:
numeric_state_config[CONF_FOR] = config[CONF_FOR]

return await numeric_state_automation.async_attach_trigger(
hass, numeric_state_config, action, automation_info, platform_type="device"
)


async def async_get_triggers(hass, device_id):
"""List device triggers."""
triggers = []
entity_registry = await hass.helpers.entity_registry.async_get_registry()

entries = [
entry
for entry in async_entries_for_device(entity_registry, device_id)
if entry.domain == DOMAIN
]

for entry in entries:
device_class = DEVICE_CLASS_NONE
state = hass.states.get(entry.entity_id)
if state:
device_class = state.attributes.get(ATTR_DEVICE_CLASS)

templates = ENTITY_TRIGGERS.get(
device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE]
)

triggers.extend(
(
{
**automation,
"platform": "device",
"device_id": device_id,
"entity_id": entry.entity_id,
"domain": DOMAIN,
}
for automation in templates
)
)

return triggers


async def async_get_trigger_capabilities(hass, trigger):
"""List trigger capabilities."""
return {
"extra_fields": vol.Schema(
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
)
}
26 changes: 26 additions & 0 deletions homeassistant/components/sensor/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"device_automation": {
"condition_type": {
"is_battery_level": "{entity_name} battery level",
"is_humidity": "{entity_name} humidity",
"is_illuminance": "{entity_name} illuminance",
"is_power": "{entity_name} power",
"is_pressure": "{entity_name} pressure",
"is_signal_strength": "{entity_name} signal strength",
"is_temperature": "{entity_name} temperature",
"is_timestamp": "{entity_name} timestamp",
"is_value": "{entity_name} value"
},
"trigger_type": {
"battery_level": "{entity_name} battery level",
"humidity": "{entity_name} humidity",
"illuminance": "{entity_name} illuminance",
"power": "{entity_name} power",
"pressure": "{entity_name} pressure",
"signal_strength": "{entity_name} signal strength",
"temperature": "{entity_name} temperature",
"timestamp": "{entity_name} timestamp",
"value": "{entity_name} value"
}
}
}
83 changes: 83 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test the helper method for writing tests."""
import asyncio
import collections
import functools as ft
import json
import logging
Expand Down Expand Up @@ -1050,3 +1051,85 @@ def mock_signal_handler(*args):
hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler)

return calls


class hashdict(dict):
"""
hashable dict implementation, suitable for use as a key into other dicts.

>>> h1 = hashdict({"apples": 1, "bananas":2})
>>> h2 = hashdict({"bananas": 3, "mangoes": 5})
>>> h1+h2
hashdict(apples=1, bananas=3, mangoes=5)
>>> d1 = {}
>>> d1[h1] = "salad"
>>> d1[h1]
'salad'
>>> d1[h2]
Traceback (most recent call last):
...
KeyError: hashdict(bananas=3, mangoes=5)

based on answers from
http://stackoverflow.com/questions/1151658/python-hashable-dicts

"""

def __key(self): # noqa: D105 no docstring
return tuple(sorted(self.items()))

def __repr__(self): # noqa: D105 no docstring
return ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key())

def __hash__(self): # noqa: D105 no docstring
return hash(self.__key())

def __setitem__(self, key, value): # noqa: D105 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

def __delitem__(self, key): # noqa: D105 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

def clear(self): # noqa: D102 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

def pop(self, *args, **kwargs): # noqa: D102 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

def popitem(self, *args, **kwargs): # noqa: D102 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

def setdefault(self, *args, **kwargs): # noqa: D102 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

def update(self, *args, **kwargs): # noqa: D102 no docstring
raise TypeError(
"{0} does not support item assignment".format(self.__class__.__name__)
)

# update is not ok because it mutates the object
# __add__ is ok because it creates a new object
# while the new object is under construction, it's ok to mutate it
def __add__(self, right): # noqa: D105 no docstring
result = hashdict(self)
dict.update(result, right)
return result


def assert_lists_same(a, b):
"""Compare two lists, ignoring order."""
assert collections.Counter([hashdict(i) for i in a]) == collections.Counter(
[hashdict(i) for i in b]
)
Comment on lines +1131 to +1135
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to compare lists with different order since the order of automations is not deterministic when we have automations from different domains.

11 changes: 9 additions & 2 deletions tests/components/deconz/test_device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from homeassistant.components.deconz import device_trigger

from tests.common import async_get_device_automations
from tests.common import assert_lists_same, async_get_device_automations

from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration

Expand Down Expand Up @@ -83,6 +83,13 @@ async def test_get_triggers(hass):
"type": device_trigger.CONF_LONG_RELEASE,
"subtype": device_trigger.CONF_TURN_OFF,
},
{
"device_id": device_id,
"domain": "sensor",
"entity_id": "sensor.tradfri_on_off_switch_battery_level",
"platform": "device",
"type": "battery_level",
},
]

assert triggers == expected_triggers
assert_lists_same(triggers, expected_triggers)
Loading