Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Wildcard support for entity_id lists #12380

Closed
wants to merge 3 commits into from
Closed
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
35 changes: 29 additions & 6 deletions homeassistant/components/sensor/min_max.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
CONF_NAME, STATE_UNKNOWN, CONF_TYPE, ATTR_UNIT_OF_MEASUREMENT)
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_pattern_matching \
import EntityPatternMatching as EPM
from homeassistant.helpers.event import async_track_state_change

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -49,21 +51,23 @@
vol.Optional(CONF_TYPE, default=SENSOR_TYPES[ATTR_MAX_VALUE]):
vol.All(cv.string, vol.In(SENSOR_TYPES.values())),
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_ENTITY_IDS): cv.entity_ids,
# vol.Required(CONF_ENTITY_IDS): cv.entity_ids,
vol.Required(CONF_ENTITY_IDS): cv.entity_id_pattern_list,
vol.Optional(CONF_ROUND_DIGITS, default=2): vol.Coerce(int),
})


@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the min/max/mean sensor."""
entity_ids = config.get(CONF_ENTITY_IDS)
entities_patterns = config.get(CONF_ENTITY_IDS)
name = config.get(CONF_NAME)
sensor_type = config.get(CONF_TYPE)
round_digits = config.get(CONF_ROUND_DIGITS)

async_add_devices(
[MinMaxSensor(hass, entity_ids, name, sensor_type, round_digits)],
[MinMaxSensor(hass, entities_patterns, name, sensor_type,
round_digits)],
True)
return True

Expand Down Expand Up @@ -104,12 +108,15 @@ def calc_mean(sensor_values, round_digits):
class MinMaxSensor(Entity):
"""Representation of a min/max sensor."""

def __init__(self, hass, entity_ids, name, sensor_type, round_digits):
def __init__(self, hass, entities_patterns, name, sensor_type,
round_digits):
"""Initialize the min/max sensor."""
self._hass = hass
self._entity_ids = entity_ids
self._entity_ids = entities_patterns[0]
# self._entity_ids = entity_ids
self._sensor_type = sensor_type
self._round_digits = round_digits
_LOGGER.debug(entities_patterns)

if name:
self._name = name
Expand All @@ -120,6 +127,7 @@ def __init__(self, hass, entity_ids, name, sensor_type, round_digits):
self._unit_of_measurement = None
self._unit_of_measurement_mismatch = False
self.min_value = self.max_value = self.mean = self.last = STATE_UNKNOWN
# self.count_sensors = len(self._entity_ids)
self.count_sensors = len(self._entity_ids)
self.states = {}

Expand Down Expand Up @@ -152,8 +160,23 @@ def async_min_max_sensor_state_listener(entity, old_state, new_state):

hass.async_add_job(self.async_update_ha_state, True)

@callback
def update_entity_ids(new_entity_ids):
"""Update entity_ids after pattern matching."""
_LOGGER.debug(new_entity_ids)
async_track_state_change(
hass, new_entity_ids, async_min_max_sensor_state_listener)
self._entity_ids.update(new_entity_ids)
self.count_sensors += len(new_entity_ids)
hass.async_add_job(self.async_update_ha_state, True)
for entity in self._entity_ids:
self.states[entity] = \
float(self._hass.states.get(entity).state)

EPM(hass, update_entity_ids, entities_patterns)

async_track_state_change(
hass, entity_ids, async_min_max_sensor_state_listener)
hass, self._entity_ids, async_min_max_sensor_state_listener)

@property
def name(self):
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

# Pattern for validating entity IDs (format: <domain>.<entity>)
ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$")
# Pattern for validating entity ID patterns using fnmatch
ENTITY_ID_FNMATCH_PATTERN = re.compile(r"^(\S*(\*|\?|\[\S*\])\S*)$")

# How long to wait till things that run on startup have to finish.
TIMEOUT_EVENT_START = 15
Expand All @@ -65,6 +67,11 @@ def valid_entity_id(entity_id: str) -> bool:
return ENTITY_ID_PATTERN.match(entity_id) is not None


def valid_entity_id_fnmatch_pattern(entity_id: str) -> bool:
"""Test if an entity ID pattern is a valid format."""
return ENTITY_ID_FNMATCH_PATTERN.match(entity_id) is not None


def valid_state(state: str) -> bool:
"""Test if a state is valid."""
return len(state) < 256
Expand Down
26 changes: 24 additions & 2 deletions homeassistant/helpers/config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import logging
import inspect

from typing import Any, Union, TypeVar, Callable, Sequence, Dict
from typing import Any, Union, TypeVar, Callable, Sequence, Dict, Tuple

import voluptuous as vol

Expand All @@ -18,7 +18,8 @@
CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS,
CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET,
SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC)
from homeassistant.core import valid_entity_id
from homeassistant.core import (
valid_entity_id, valid_entity_id_fnmatch_pattern)
from homeassistant.exceptions import TemplateError
import homeassistant.util.dt as dt_util
from homeassistant.util import slugify as util_slugify
Expand Down Expand Up @@ -147,6 +148,27 @@ def entity_ids(value: Union[str, Sequence]) -> Sequence[str]:
return [entity_id(ent_id) for ent_id in value]


def entity_id_pattern_list(value: Union[T, Sequence[T]]) \
-> Tuple[Sequence[T], Sequence[T]]:
"""Validate List entries to be Entity ID or pattern."""
if value is None:
return ([], [])
ids = set()
patterns = set()
if not isinstance(value, list):
value = [value]
for e in value:
e = e.strip().lower()
if valid_entity_id_fnmatch_pattern(e):
patterns.add(e)
elif valid_entity_id(e):
ids.add(e)
else:
raise vol.Invalid('Entity ID or pattern {} is an invalid entity id'
'or pattern'.format(e))
return (ids, patterns)


def enum(enumClass):
"""Create validator for specified enum."""
return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__)
Expand Down
46 changes: 46 additions & 0 deletions homeassistant/helpers/entity_pattern_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""A Class to handle matching entity_ids through patterns."""
import fnmatch
import re

from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components.zwave.const import EVENT_NETWORK_READY


class EntityPatternMatching(object):
"""Class to generate patterns and match entity_ids."""

def __init__(self, hass, action, entities_patterns):
"""Initialize and EntityPatternMatching (EPM) object."""
self.hass = hass
self.action = action
self.entity_ids = set()
self.entity_ids_matched = entities_patterns[0]
self.patterns = [re.compile(fnmatch.translate(pattern))
for pattern in entities_patterns[1]]

self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, self.report_matched_entity_ids)
self.hass.bus.async_listen_once(
EVENT_NETWORK_READY, self.report_matched_entity_ids)

def match_entity_ids(self):
"""Match entity_ids through saved patterns."""
all_ids = self.hass.states.async_entity_ids()
for entity in self.entity_ids_matched:
if entity in all_ids:
all_ids.remove(entity)

for pattern in self.patterns:
for entity in all_ids[:]:
if pattern.match(entity):
all_ids.remove(entity)
self.entity_ids.add(entity)
self.entity_ids_matched.add(entity)

@callback
def report_matched_entity_ids(self, event):
"""Report all matched entity_ids."""
self.match_entity_ids()
self.hass.async_run_job(self.action, self.entity_ids)
self.entity_ids.clear()
19 changes: 18 additions & 1 deletion tests/helpers/test_config_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ def test_entity_ids():
]


def test_entity_id_pattern_list():
"""Test entity_id_pattern_list."""
schema = vol.Schema(cv.entity_id_pattern_list)
with pytest.raises(vol.MultipleInvalid):
schema('a sensor.*')
schema('sensor?light f')
schema('sensor[a.light')
assert ([], []) == schema(None)
assert ({'sensor.light'}, set()) == schema('sensor.LIGHT ')
assert ({'sensor.light'}, {'sensor.*'}) == \
schema(['sensor.light', 'sensor.*'])
assert (set(), {'sen?or.light', 'sensor.*', 'sen[s]or.light'}) == \
schema(['sen?or.light', 'sensor.*', 'sen[s]or.light'])
assert ({'sensor.light'}, set()) == \
schema(['sensor.light', 'sensor.light'])


def test_ensure_list_csv():
"""Test ensure_list_csv."""
schema = vol.Schema(cv.ensure_list_csv)
Expand Down Expand Up @@ -453,7 +470,7 @@ def test_deprecated(caplog):
)

deprecated_schema({'venus': True})
assert len(caplog.records) == 0
assert caplog.records == []

deprecated_schema({'mars': True})
assert len(caplog.records) == 1
Expand Down
57 changes: 57 additions & 0 deletions tests/helpers/test_entity_pattern_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""The tests for the entity_patter_matching helper class."""
import unittest

from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components.zwave.const import EVENT_NETWORK_READY
from homeassistant.helpers.entity_pattern_matching \
import EntityPatternMatching as EPM

from tests.common import get_test_home_assistant


class TestEntityPatternMatching(unittest.TestCase):
"""Test the EntityPatterMatching helper class."""

def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()

def tearDown(self):
"""Stop down everthing that was started."""
self.hass.stop()

def test_entity_pattern_matching(self):
"""Test if entity pattern matching works correctly."""
entity_ids = set()
update_runs = []
entities_patterns = ({'light.demo'}, {'light.d*', 'light.b*',
'lig[h]t.demo2', 'light?demo3'})

@callback
def update_entity_ids(new_entity_ids):
"""Method called from EPM to report new entity ids."""
update_runs.append(new_entity_ids.copy())
for entity in new_entity_ids:
self.assertFalse(entity in entity_ids)
entity_ids.update(new_entity_ids)

EPM(self.hass, update_entity_ids, entities_patterns)

self.assertEqual(0, len(update_runs))

self.hass.states.set('light.Bowl', 'on')
self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
self.hass.block_till_done()

self.assertEqual(1, len(update_runs))
self.assertEqual(update_runs[0], {'light.bowl'})

self.hass.states.set('light.demo2', 'off')
self.hass.states.set('light.demo3', 'off')
self.hass.states.set('cover.living_room', 'open')
self.hass.bus.fire(EVENT_NETWORK_READY)
self.hass.block_till_done()

self.assertEqual(2, len(update_runs))
self.assertEqual(update_runs[1], {'light.demo2', 'light.demo3'})