From 2ec0d25a3892b617aee3df9fd6ce7bc470f1e734 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Fri, 25 Aug 2017 03:33:53 -0700 Subject: [PATCH] optimistic mode for template covers (w/o timed movement) (#8402) * Emulate set_current_position in cover.template * Add opportunistic mode * Prevent another move when cover is already moving. Add tests for opotunistic/timed-delay mode * Remove timed-move capabilities * Set init state to unknown * cleanup template * Update test_template.py --- homeassistant/components/cover/template.py | 60 +++++++----- tests/components/cover/test_template.py | 107 +++++++++++++++------ 2 files changed, 118 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index 769c2fc4ed6..f9e059d3927 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, MATCH_ALL, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, - STATE_OPEN, STATE_CLOSED) + CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id @@ -39,6 +39,8 @@ CLOSE_ACTION = 'close_cover' STOP_ACTION = 'stop_cover' POSITION_ACTION = 'set_cover_position' TILT_ACTION = 'set_cover_tilt_position' +CONF_TILT_OPTIMISTIC = 'tilt_optimistic' + CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position' CONF_OPEN_OR_CLOSE = 'open_or_close' @@ -56,6 +58,8 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string, @@ -83,11 +87,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): stop_action = device_config.get(STOP_ACTION) position_action = device_config.get(POSITION_ACTION) tilt_action = device_config.get(TILT_ACTION) - - if position_template is None and state_template is None: - _LOGGER.error('Must specify either %s' or '%s', - CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE) - continue + optimistic = device_config.get(CONF_OPTIMISTIC) + tilt_optimistic = device_config.get(CONF_TILT_OPTIMISTIC) if position_action is None and open_action is None: _LOGGER.error('Must specify at least one of %s' or '%s', @@ -125,7 +126,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device, friendly_name, state_template, position_template, tilt_template, icon_template, open_action, close_action, stop_action, - position_action, tilt_action, entity_ids + position_action, tilt_action, + optimistic, tilt_optimistic, entity_ids ) ) if not covers: @@ -142,7 +144,8 @@ class CoverTemplate(CoverDevice): def __init__(self, hass, device_id, friendly_name, state_template, position_template, tilt_template, icon_template, open_action, close_action, stop_action, - position_action, tilt_action, entity_ids): + position_action, tilt_action, + optimistic, tilt_optimistic, entity_ids): """Initialize the Template cover.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -167,6 +170,9 @@ class CoverTemplate(CoverDevice): self._tilt_script = None if tilt_action is not None: self._tilt_script = Script(hass, tilt_action) + self._optimistic = (optimistic or + (not state_template and not position_template)) + self._tilt_optimistic = tilt_optimistic or not tilt_template self._icon = None self._position = None self._tilt_value = None @@ -260,19 +266,23 @@ class CoverTemplate(CoverDevice): def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: - self.hass.async_add_job(self._open_script.async_run()) + yield from self._open_script.async_run() elif self._position_script: - self.hass.async_add_job(self._position_script.async_run( - {"position": 100})) + yield from self._position_script.async_run({"position": 100}) + if self._optimistic: + self._position = 100 + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_close_cover(self, **kwargs): """Move the cover down.""" if self._close_script: - self.hass.async_add_job(self._close_script.async_run()) + yield from self._close_script.async_run() elif self._position_script: - self.hass.async_add_job(self._position_script.async_run( - {"position": 0})) + yield from self._position_script.async_run({"position": 0}) + if self._optimistic: + self._position = 0 + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_stop_cover(self, **kwargs): @@ -284,29 +294,35 @@ class CoverTemplate(CoverDevice): def async_set_cover_position(self, **kwargs): """Set cover position.""" self._position = kwargs[ATTR_POSITION] - self.hass.async_add_job(self._position_script.async_run( - {"position": self._position})) + yield from self._position_script.async_run( + {"position": self._position}) + if self._optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" self._tilt_value = 100 - self.hass.async_add_job(self._tilt_script.async_run( - {"tilt": self._tilt_value})) + yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + if self._tilt_optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" self._tilt_value = 0 - self.hass.async_add_job(self._tilt_script.async_run( - {"tilt": self._tilt_value})) + yield from self._tilt_script.async_run( + {"tilt": self._tilt_value}) + if self._tilt_optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - self.hass.async_add_job(self._tilt_script.async_run( - {"tilt": self._tilt_value})) + yield from self._tilt_script.async_run({"tilt": self._tilt_value}) + if self._tilt_optimistic: + self.hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine def async_update(self): diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index cd2120e71e6..3c574bbf497 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -21,7 +21,7 @@ class TestTemplateCover(unittest.TestCase): # pylint: disable=invalid-name def setup_method(self, method): - """Setup things to be run when tests are started.""" + """Initialize services when tests are started.""" self.hass = get_test_home_assistant() self.calls = [] @@ -254,32 +254,6 @@ class TestTemplateCover(unittest.TestCase): assert self.hass.states.all() == [] - def test_template_position_or_value(self): - """Test that at least one of value or position template is used.""" - with assert_setup_component(1, 'cover'): - assert setup.setup_component(self.hass, 'cover', { - 'cover': { - 'platform': 'template', - 'covers': { - 'test_template_cover': { - 'open_cover': { - 'service': 'cover.open_cover', - 'entity_id': 'cover.test_state' - }, - 'close_cover': { - 'service': 'cover.close_cover', - 'entity_id': 'cover.test_state' - }, - }, - } - } - }) - - self.hass.start() - self.hass.block_till_done() - - assert self.hass.states.all() == [] - def test_template_open_or_position(self): """Test that at least one of open_cover or set_position is used.""" with assert_setup_component(1, 'cover'): @@ -590,6 +564,85 @@ class TestTemplateCover(unittest.TestCase): assert len(self.calls) == 1 + def test_set_position_optimistic(self): + """Test optimistic position mode.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'set_cover_position': { + 'service': 'test.automation', + }, + } + } + } + }) + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') is None + + cover.set_cover_position(self.hass, 42, + 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_position') == 42.0 + + cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_CLOSED + + cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.state == STATE_OPEN + + def test_set_tilt_position_optimistic(self): + """Test the optimistic tilt_position mode.""" + with assert_setup_component(1, 'cover'): + assert setup.setup_component(self.hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'position_template': + "{{ 100 }}", + 'set_cover_position': { + 'service': 'test.automation', + }, + 'set_cover_tilt_position': { + 'service': 'test.automation', + }, + } + } + } + }) + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') is None + + cover.set_cover_tilt_position(self.hass, 42, + 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') == 42.0 + + cover.close_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') == 0.0 + + cover.open_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.block_till_done() + state = self.hass.states.get('cover.test_template_cover') + assert state.attributes.get('current_tilt_position') == 100.0 + def test_icon_template(self): """Test icon template.""" with assert_setup_component(1, 'cover'):