Automation: initial state > restore state (#6911)

* Automation: initial state > restore state

* Clean up code

* Ensure MQTT defaults are used.

* Ensure failed platforms always return None

* Automation: write state to state machine after start
This commit is contained in:
Paulus Schoutsen 2017-04-03 23:11:39 -07:00 committed by GitHub
parent 3895979e39
commit 23645da74c
4 changed files with 235 additions and 103 deletions

View File

@ -82,8 +82,7 @@ _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string, CONF_ALIAS: cv.string,
vol.Optional(CONF_INITIAL_STATE, vol.Optional(CONF_INITIAL_STATE): cv.boolean,
default=DEFAULT_INITIAL_STATE): cv.boolean,
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
@ -102,15 +101,13 @@ TRIGGER_SERVICE_SCHEMA = vol.Schema({
RELOAD_SERVICE_SCHEMA = vol.Schema({}) RELOAD_SERVICE_SCHEMA = vol.Schema({})
def is_on(hass, entity_id=None): def is_on(hass, entity_id):
""" """
Return true if specified automation entity_id is on. Return true if specified automation entity_id is on.
Check all automation if no entity_id specified. Async friendly.
""" """
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) return hass.states.is_state(entity_id, STATE_ON)
return any(hass.states.is_state(entity_id, STATE_ON)
for entity_id in entity_ids)
def turn_on(hass, entity_id=None): def turn_on(hass, entity_id=None):
@ -232,7 +229,6 @@ class AutomationEntity(ToggleEntity):
self._async_detach_triggers = None self._async_detach_triggers = None
self._cond_func = cond_func self._cond_func = cond_func
self._async_action = async_action self._async_action = async_action
self._enabled = False
self._last_triggered = None self._last_triggered = None
self._hidden = hidden self._hidden = hidden
self._initial_state = initial_state self._initial_state = initial_state
@ -262,24 +258,26 @@ class AutomationEntity(ToggleEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if entity is on.""" """Return True if entity is on."""
return self._enabled return self._async_detach_triggers is not None
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self) -> None: def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state.""" """Startup with initial state or previous state."""
enable_automation = False enable_automation = DEFAULT_INITIAL_STATE
state = yield from async_get_last_state(self.hass, self.entity_id) if self._initial_state is not None:
if state is None: enable_automation = self._initial_state
if self._initial_state:
enable_automation = True
else: else:
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered') self._last_triggered = state.attributes.get('last_triggered')
if state.state == STATE_ON:
enable_automation = True
# HomeAssistant is on bootstrap if not enable_automation:
if enable_automation and self.hass.state == CoreState.not_running: return
# HomeAssistant is starting up
elif self.hass.state == CoreState.not_running:
@asyncio.coroutine @asyncio.coroutine
def async_enable_automation(event): def async_enable_automation(event):
"""Start automation on startup.""" """Start automation on startup."""
@ -289,27 +287,25 @@ class AutomationEntity(ToggleEntity):
EVENT_HOMEASSISTANT_START, async_enable_automation) EVENT_HOMEASSISTANT_START, async_enable_automation)
# HomeAssistant is running # HomeAssistant is running
elif enable_automation: else:
yield from self.async_enable() yield from self.async_enable()
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self, **kwargs) -> None: def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on and update the state.""" """Turn the entity on and update the state."""
if self._enabled: if self.is_on:
return return
yield from self.async_enable() yield from self.async_enable()
yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self, **kwargs) -> None: def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off.""" """Turn the entity off."""
if not self._enabled: if not self.is_on:
return return
self._async_detach_triggers() self._async_detach_triggers()
self._async_detach_triggers = None self._async_detach_triggers = None
self._enabled = False
yield from self.async_update_ha_state() yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
@ -335,12 +331,12 @@ class AutomationEntity(ToggleEntity):
This method is a coroutine. This method is a coroutine.
""" """
if self._enabled: if self.is_on:
return return
self._async_detach_triggers = yield from self._async_attach_triggers( self._async_detach_triggers = yield from self._async_attach_triggers(
self.async_trigger) self.async_trigger)
self._enabled = True yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
@ -359,7 +355,7 @@ def _async_process_config(hass, config, component):
list_no) list_no)
hidden = config_block[CONF_HIDE_ENTITY] hidden = config_block[CONF_HIDE_ENTITY]
initial_state = config_block[CONF_INITIAL_STATE] initial_state = config_block.get(CONF_INITIAL_STATE)
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
name) name)

View File

@ -277,7 +277,10 @@ def _async_setup_discovery(hass, config):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Start the MQTT protocol service.""" """Start the MQTT protocol service."""
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN)
if conf is None:
conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
client_id = conf.get(CONF_CLIENT_ID) client_id = conf.get(CONF_CLIENT_ID)
keepalive = conf.get(CONF_KEEPALIVE) keepalive = conf.get(CONF_KEEPALIVE)

View File

@ -253,7 +253,7 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
if not dep_success: if not dep_success:
log_error('Could not setup all dependencies.') log_error('Could not setup all dependencies.')
return False return None
if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'): if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'):
req_success = yield from _async_process_requirements( req_success = yield from _async_process_requirements(

View File

@ -14,7 +14,7 @@ import homeassistant.util.dt as dt_util
from tests.common import ( from tests.common import (
assert_setup_component, get_test_home_assistant, fire_time_changed, assert_setup_component, get_test_home_assistant, fire_time_changed,
mock_component, mock_service, mock_restore_cache) mock_service, mock_restore_cache)
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -24,7 +24,6 @@ class TestAutomation(unittest.TestCase):
def setUp(self): def setUp(self):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
mock_component(self.hass, 'group')
self.calls = mock_service(self.hass, 'test', 'automation') self.calls = mock_service(self.hass, 'test', 'automation')
def tearDown(self): def tearDown(self):
@ -156,46 +155,6 @@ class TestAutomation(unittest.TestCase):
self.assertEqual(['hello.world'], self.assertEqual(['hello.world'],
self.calls[0].data.get(ATTR_ENTITY_ID)) self.calls[0].data.get(ATTR_ENTITY_ID))
def test_service_initial_value_off(self):
"""Test initial value off."""
entity_id = 'automation.hello'
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'initial_state': 'off',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': ['hello.world', 'hello.world2']
}
}
})
assert not automation.is_on(self.hass, entity_id)
def test_service_initial_value_on(self):
"""Test initial value on."""
entity_id = 'automation.hello'
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'initial_state': 'on',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': ['hello.world', 'hello.world2']
}
}
})
assert automation.is_on(self.hass, entity_id)
def test_service_specify_entity_id_list(self): def test_service_specify_entity_id_list(self):
"""Test service data.""" """Test service data."""
assert setup_component(self.hass, automation.DOMAIN, { assert setup_component(self.hass, automation.DOMAIN, {
@ -569,38 +528,6 @@ class TestAutomation(unittest.TestCase):
self.hass.block_till_done() self.hass.block_till_done()
assert len(self.calls) == 2 assert len(self.calls) == 2
def test_automation_not_trigger_on_bootstrap(self):
"""Test if automation is not trigger on bootstrap."""
self.hass.state = CoreState.not_running
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
self.hass.bus.fire('test_event')
self.hass.block_till_done()
assert len(self.calls) == 0
self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
self.hass.block_till_done()
self.hass.states = CoreState.running
self.hass.bus.fire('test_event')
self.hass.block_till_done()
assert len(self.calls) == 1
assert ['hello.world'] == self.calls[0].data.get(ATTR_ENTITY_ID)
@asyncio.coroutine @asyncio.coroutine
def test_automation_restore_state(hass): def test_automation_restore_state(hass):
@ -653,3 +580,209 @@ def test_automation_restore_state(hass):
yield from hass.async_block_till_done() yield from hass.async_block_till_done()
assert len(calls) == 1 assert len(calls) == 1
@asyncio.coroutine
def test_initial_value_off(hass):
"""Test initial value off."""
calls = mock_service(hass, 'test', 'automation')
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'initial_state': 'off',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
assert res
assert not automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 0
@asyncio.coroutine
def test_initial_value_on(hass):
"""Test initial value on."""
calls = mock_service(hass, 'test', 'automation')
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'initial_state': 'on',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': ['hello.world', 'hello.world2']
}
}
})
assert res
assert automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 1
@asyncio.coroutine
def test_initial_value_off_but_restore_on(hass):
"""Test initial value off and restored state is turned on."""
calls = mock_service(hass, 'test', 'automation')
mock_restore_cache(hass, (
State('automation.hello', STATE_ON),
))
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'initial_state': 'off',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
assert res
assert not automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 0
@asyncio.coroutine
def test_initial_value_on_but_restore_off(hass):
"""Test initial value on and restored state is turned off."""
calls = mock_service(hass, 'test', 'automation')
mock_restore_cache(hass, (
State('automation.hello', STATE_OFF),
))
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'initial_state': 'on',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
assert res
assert automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 1
@asyncio.coroutine
def test_no_initial_value_and_restore_off(hass):
"""Test initial value off and restored state is turned on."""
calls = mock_service(hass, 'test', 'automation')
mock_restore_cache(hass, (
State('automation.hello', STATE_OFF),
))
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
assert res
assert not automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 0
@asyncio.coroutine
def test_automation_is_on_if_no_initial_state_or_restore(hass):
"""Test initial value is on when no initial state or restored state."""
calls = mock_service(hass, 'test', 'automation')
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
assert res
assert automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 1
@asyncio.coroutine
def test_automation_not_trigger_on_bootstrap(hass):
"""Test if automation is not trigger on bootstrap."""
hass.state = CoreState.not_running
calls = mock_service(hass, 'test', 'automation')
res = yield from async_setup_component(hass, automation.DOMAIN, {
automation.DOMAIN: {
'alias': 'hello',
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'action': {
'service': 'test.automation',
'entity_id': 'hello.world'
}
}
})
assert res
assert not automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 0
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
yield from hass.async_block_till_done()
assert automation.is_on(hass, 'automation.hello')
hass.bus.async_fire('test_event')
yield from hass.async_block_till_done()
assert len(calls) == 1
assert ['hello.world'] == calls[0].data.get(ATTR_ENTITY_ID)