Add recent context (#15989)

* Add recent context

* Add async_set_context to components not using new services
This commit is contained in:
Paulus Schoutsen 2018-08-20 17:39:53 +02:00 committed by GitHub
parent d1e1b9b38a
commit 1be61df9c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 211 additions and 11 deletions

View file

@ -111,6 +111,7 @@ def async_setup(hass, config):
for alert_id in alert_ids: for alert_id in alert_ids:
alert = all_alerts[alert_id] alert = all_alerts[alert_id]
alert.async_set_context(service_call.context)
if service_call.service == SERVICE_TURN_ON: if service_call.service == SERVICE_TURN_ON:
yield from alert.async_turn_on() yield from alert.async_turn_on()
elif service_call.service == SERVICE_TOGGLE: elif service_call.service == SERVICE_TOGGLE:

View file

@ -345,6 +345,8 @@ async def async_setup(hass, config):
update_tasks = [] update_tasks = []
for light in target_lights: for light in target_lights:
light.async_set_context(service.context)
pars = params pars = params
if not pars: if not pars:
pars = params.copy() pars = params.copy()
@ -356,7 +358,7 @@ async def async_setup(hass, config):
continue continue
update_tasks.append( update_tasks.append(
light.async_update_ha_state(True, service.context)) light.async_update_ha_state(True))
if update_tasks: if update_tasks:
await asyncio.wait(update_tasks, loop=hass.loop) await asyncio.wait(update_tasks, loop=hass.loop)

View file

@ -1,5 +1,6 @@
"""An abstract class for entities.""" """An abstract class for entities."""
import asyncio import asyncio
from datetime import timedelta
import logging import logging
import functools as ft import functools as ft
from timeit import default_timer as timer from timeit import default_timer as timer
@ -16,6 +17,7 @@ from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.util import ensure_unique_string, slugify from homeassistant.util import ensure_unique_string, slugify
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SLOW_UPDATE_WARNING = 10 SLOW_UPDATE_WARNING = 10
@ -85,6 +87,10 @@ class Entity:
# Hold list for functions to call on remove. # Hold list for functions to call on remove.
_on_remove = None _on_remove = None
# Context
_context = None
_context_set = None
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
"""Return True if entity has to be polled for state. """Return True if entity has to be polled for state.
@ -173,13 +179,24 @@ class Entity:
"""Flag supported features.""" """Flag supported features."""
return None return None
@property
def context_recent_time(self):
"""Time that a context is considered recent."""
return timedelta(seconds=5)
# DO NOT OVERWRITE # DO NOT OVERWRITE
# These properties and methods are either managed by Home Assistant or they # These properties and methods are either managed by Home Assistant or they
# are used to perform a very specific function. Overwriting these may # are used to perform a very specific function. Overwriting these may
# produce undesirable effects in the entity's operation. # produce undesirable effects in the entity's operation.
@callback
def async_set_context(self, context):
"""Set the context the entity currently operates under."""
self._context = context
self._context_set = dt_util.utcnow()
@asyncio.coroutine @asyncio.coroutine
def async_update_ha_state(self, force_refresh=False, context=None): def async_update_ha_state(self, force_refresh=False):
"""Update Home Assistant with current state of entity. """Update Home Assistant with current state of entity.
If force_refresh == True will update entity before setting state. If force_refresh == True will update entity before setting state.
@ -278,8 +295,14 @@ class Entity:
# Could not convert state to float # Could not convert state to float
pass pass
if (self._context is not None and
dt_util.utcnow() - self._context_set >
self.context_recent_time):
self._context = None
self._context_set = None
self.hass.states.async_set( self.hass.states.async_set(
self.entity_id, state, attr, self.force_update, context) self.entity_id, state, attr, self.force_update, self._context)
def schedule_update_ha_state(self, force_refresh=False): def schedule_update_ha_state(self, force_refresh=False):
"""Schedule an update ha state change task. """Schedule an update ha state change task.

View file

@ -218,13 +218,15 @@ async def _handle_service_platform_call(func, data, entities, context):
if not entity.available: if not entity.available:
continue continue
entity.async_set_context(context)
if isinstance(func, str): if isinstance(func, str):
await getattr(entity, func)(**data) await getattr(entity, func)(**data)
else: else:
await func(entity, data) await func(entity, data)
if entity.should_poll: if entity.should_poll:
tasks.append(entity.async_update_ha_state(True, context)) tasks.append(entity.async_update_ha_state(True))
if tasks: if tasks:
await asyncio.wait(tasks) await asyncio.wait(tasks)

View file

@ -4,7 +4,7 @@ import asyncio
import unittest import unittest
import logging import logging
from homeassistant.core import CoreState, State from homeassistant.core import CoreState, State, Context
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.counter import ( from homeassistant.components.counter import (
DOMAIN, decrement, increment, reset, CONF_INITIAL, CONF_STEP, CONF_NAME, DOMAIN, decrement, increment, reset, CONF_INITIAL, CONF_STEP, CONF_NAME,
@ -202,3 +202,24 @@ def test_no_initial_state_and_no_restore_state(hass):
state = hass.states.get('counter.test1') state = hass.states.get('counter.test1')
assert state assert state
assert int(state.state) == 0 assert int(state.state) == 0
async def test_counter_context(hass):
"""Test that counter context works."""
assert await async_setup_component(hass, 'counter', {
'counter': {
'test': {}
}
})
state = hass.states.get('counter.test')
assert state is not None
await hass.services.async_call('counter', 'increment', {
'entity_id': state.entity_id,
}, True, Context(user_id='abcd'))
state2 = hass.states.get('counter.test')
assert state2 is not None
assert state.state != state2.state
assert state2.context.user_id == 'abcd'

View file

@ -4,7 +4,7 @@ import asyncio
import unittest import unittest
import logging import logging
from homeassistant.core import CoreState, State from homeassistant.core import CoreState, State, Context
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.input_boolean import ( from homeassistant.components.input_boolean import (
DOMAIN, is_on, toggle, turn_off, turn_on, CONF_INITIAL) DOMAIN, is_on, toggle, turn_off, turn_on, CONF_INITIAL)
@ -158,3 +158,24 @@ def test_initial_state_overrules_restore_state(hass):
state = hass.states.get('input_boolean.b2') state = hass.states.get('input_boolean.b2')
assert state assert state
assert state.state == 'on' assert state.state == 'on'
async def test_input_boolean_context(hass):
"""Test that input_boolean context works."""
assert await async_setup_component(hass, 'input_boolean', {
'input_boolean': {
'ac': {CONF_INITIAL: True},
}
})
state = hass.states.get('input_boolean.ac')
assert state is not None
await hass.services.async_call('input_boolean', 'turn_off', {
'entity_id': state.entity_id,
}, True, Context(user_id='abcd'))
state2 = hass.states.get('input_boolean.ac')
assert state2 is not None
assert state.state != state2.state
assert state2.context.user_id == 'abcd'

View file

@ -4,7 +4,7 @@ import asyncio
import unittest import unittest
import datetime import datetime
from homeassistant.core import CoreState, State from homeassistant.core import CoreState, State, Context
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.input_datetime import ( from homeassistant.components.input_datetime import (
DOMAIN, ATTR_ENTITY_ID, ATTR_DATE, ATTR_TIME, SERVICE_SET_DATETIME) DOMAIN, ATTR_ENTITY_ID, ATTR_DATE, ATTR_TIME, SERVICE_SET_DATETIME)
@ -208,3 +208,27 @@ def test_restore_state(hass):
state_bogus = hass.states.get('input_datetime.test_bogus_data') state_bogus = hass.states.get('input_datetime.test_bogus_data')
assert state_bogus.state == str(initial) assert state_bogus.state == str(initial)
async def test_input_datetime_context(hass):
"""Test that input_datetime context works."""
assert await async_setup_component(hass, 'input_datetime', {
'input_datetime': {
'only_date': {
'has_date': True,
}
}
})
state = hass.states.get('input_datetime.only_date')
assert state is not None
await hass.services.async_call('input_datetime', 'set_datetime', {
'entity_id': state.entity_id,
'date': '2018-01-02'
}, True, Context(user_id='abcd'))
state2 = hass.states.get('input_datetime.only_date')
assert state2 is not None
assert state.state != state2.state
assert state2.context.user_id == 'abcd'

View file

@ -3,7 +3,7 @@
import asyncio import asyncio
import unittest import unittest
from homeassistant.core import CoreState, State from homeassistant.core import CoreState, State, Context
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.input_number import ( from homeassistant.components.input_number import (
DOMAIN, set_value, increment, decrement) DOMAIN, set_value, increment, decrement)
@ -236,3 +236,27 @@ def test_no_initial_state_and_no_restore_state(hass):
state = hass.states.get('input_number.b1') state = hass.states.get('input_number.b1')
assert state assert state
assert float(state.state) == 0 assert float(state.state) == 0
async def test_input_number_context(hass):
"""Test that input_number context works."""
assert await async_setup_component(hass, 'input_number', {
'input_number': {
'b1': {
'min': 0,
'max': 100,
},
}
})
state = hass.states.get('input_number.b1')
assert state is not None
await hass.services.async_call('input_number', 'increment', {
'entity_id': state.entity_id,
}, True, Context(user_id='abcd'))
state2 = hass.states.get('input_number.b1')
assert state2 is not None
assert state.state != state2.state
assert state2.context.user_id == 'abcd'

View file

@ -5,7 +5,7 @@ import unittest
from tests.common import get_test_home_assistant, mock_restore_cache from tests.common import get_test_home_assistant, mock_restore_cache
from homeassistant.core import State from homeassistant.core import State, Context
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.input_select import ( from homeassistant.components.input_select import (
ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS, ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS,
@ -276,3 +276,30 @@ def test_initial_state_overrules_restore_state(hass):
state = hass.states.get('input_select.s2') state = hass.states.get('input_select.s2')
assert state assert state
assert state.state == 'middle option' assert state.state == 'middle option'
async def test_input_select_context(hass):
"""Test that input_select context works."""
assert await async_setup_component(hass, 'input_select', {
'input_select': {
's1': {
'options': [
'first option',
'middle option',
'last option',
],
}
}
})
state = hass.states.get('input_select.s1')
assert state is not None
await hass.services.async_call('input_select', 'select_next', {
'entity_id': state.entity_id,
}, True, Context(user_id='abcd'))
state2 = hass.states.get('input_select.s1')
assert state2 is not None
assert state.state != state2.state
assert state2.context.user_id == 'abcd'

View file

@ -3,7 +3,7 @@
import asyncio import asyncio
import unittest import unittest
from homeassistant.core import CoreState, State from homeassistant.core import CoreState, State, Context
from homeassistant.setup import setup_component, async_setup_component from homeassistant.setup import setup_component, async_setup_component
from homeassistant.components.input_text import (DOMAIN, set_value) from homeassistant.components.input_text import (DOMAIN, set_value)
@ -180,3 +180,27 @@ def test_no_initial_state_and_no_restore_state(hass):
state = hass.states.get('input_text.b1') state = hass.states.get('input_text.b1')
assert state assert state
assert str(state.state) == 'unknown' assert str(state.state) == 'unknown'
async def test_input_text_context(hass):
"""Test that input_text context works."""
assert await async_setup_component(hass, 'input_text', {
'input_text': {
't1': {
'initial': 'bla',
}
}
})
state = hass.states.get('input_text.t1')
assert state is not None
await hass.services.async_call('input_text', 'set_value', {
'entity_id': state.entity_id,
'value': 'new_value',
}, True, Context(user_id='abcd'))
state2 = hass.states.get('input_text.t1')
assert state2 is not None
assert state.state != state2.state
assert state2.context.user_id == 'abcd'

View file

@ -1,11 +1,13 @@
"""Test the entity helper.""" """Test the entity helper."""
# pylint: disable=protected-access # pylint: disable=protected-access
import asyncio import asyncio
from unittest.mock import MagicMock, patch from datetime import timedelta
from unittest.mock import MagicMock, patch, PropertyMock
import pytest import pytest
import homeassistant.helpers.entity as entity import homeassistant.helpers.entity as entity
from homeassistant.core import Context
from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS
from homeassistant.config import DATA_CUSTOMIZE from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.entity_values import EntityValues
@ -412,3 +414,32 @@ async def test_async_remove_runs_callbacks(hass):
ent.async_on_remove(lambda: result.append(1)) ent.async_on_remove(lambda: result.append(1))
await ent.async_remove() await ent.async_remove()
assert len(result) == 1 assert len(result) == 1
async def test_set_context(hass):
"""Test setting context."""
context = Context()
ent = entity.Entity()
ent.hass = hass
ent.entity_id = 'hello.world'
ent.async_set_context(context)
await ent.async_update_ha_state()
assert hass.states.get('hello.world').context == context
async def test_set_context_expired(hass):
"""Test setting context."""
context = Context()
with patch.object(entity.Entity, 'context_recent_time',
new_callable=PropertyMock) as recent:
recent.return_value = timedelta(seconds=-5)
ent = entity.Entity()
ent.hass = hass
ent.entity_id = 'hello.world'
ent.async_set_context(context)
await ent.async_update_ha_state()
assert hass.states.get('hello.world').context != context
assert ent._context is None
assert ent._context_set is None