diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 288da65a4af..75a9a31bee1 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -4,10 +4,12 @@ import logging from pyhap.const import ( CATEGORY_OUTLET, CATEGORY_SWITCH) +from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id +from homeassistant.helpers.event import call_later from . import TYPES from .accessories import HomeAccessory @@ -74,21 +76,48 @@ class Switch(HomeAccessory): self._domain = split_entity_id(self.entity_id)[0] self._flag_state = False + self.activate_only = self.is_activate( + self.hass.states.get(self.entity_id)) + serv_switch = self.add_preload_service(SERV_SWITCH) self.char_on = serv_switch.configure_char( CHAR_ON, value=False, setter_callback=self.set_state) + def is_activate(self, state): + """Check if entity is activate only.""" + can_cancel = state.attributes.get(ATTR_CAN_CANCEL) + if self._domain == 'script' and not can_cancel: + return True + return False + + def reset_switch(self, *args): + """Reset switch to emulate activate click.""" + _LOGGER.debug('%s: Reset switch to off', self.entity_id) + self.char_on.set_value(0) + def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) + if self.activate_only and value == 0: + _LOGGER.debug('%s: Ignoring turn_off call', self.entity_id) + return self._flag_state = True params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(self._domain, service, params) + if self.activate_only: + call_later(self.hass, 1, self.reset_switch) + def update_state(self, new_state): """Update switch state after state changed.""" + self.activate_only = self.is_activate(new_state) + if self.activate_only: + _LOGGER.debug('%s: Ignore state change, entity is activate only', + self.entity_id) + return + current_state = (new_state.state == STATE_ON) if not self._flag_state: _LOGGER.debug('%s: Set current state to %s', diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index d170647d492..c94929e560c 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,14 +1,18 @@ """Test different accessory types: Switches.""" +from datetime import timedelta + import pytest from homeassistant.components.homekit.const import ( ATTR_VALUE, TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE) from homeassistant.components.homekit.type_switches import ( Outlet, Switch, Valve) +from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON from homeassistant.core import split_entity_id +import homeassistant.util.dt as dt_util -from tests.common import async_mock_service +from tests.common import async_fire_time_changed, async_mock_service async def test_outlet_set_state(hass, hk_driver, events): @@ -54,18 +58,18 @@ async def test_outlet_set_state(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] is None -@pytest.mark.parametrize('entity_id', [ - 'automation.test', - 'input_boolean.test', - 'remote.test', - 'script.test', - 'switch.test', +@pytest.mark.parametrize('entity_id, attrs', [ + ('automation.test', {}), + ('input_boolean.test', {}), + ('remote.test', {}), + ('script.test', {ATTR_CAN_CANCEL: True}), + ('switch.test', {}), ]) -async def test_switch_set_state(hass, hk_driver, entity_id, events): +async def test_switch_set_state(hass, hk_driver, entity_id, attrs, events): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] - hass.states.async_set(entity_id, None) + hass.states.async_set(entity_id, None, attrs) await hass.async_block_till_done() acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -74,13 +78,14 @@ async def test_switch_set_state(hass, hk_driver, entity_id, events): assert acc.aid == 2 assert acc.category == 8 # Switch + assert acc.activate_only is False assert acc.char_on.value is False - hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_ON, attrs) await hass.async_block_till_done() assert acc.char_on.value is True - hass.states.async_set(entity_id, STATE_OFF) + hass.states.async_set(entity_id, STATE_OFF, attrs) await hass.async_block_till_done() assert acc.char_on.value is False @@ -172,3 +177,65 @@ async def test_valve_set_state(hass, hk_driver, events): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 assert events[-1].data[ATTR_VALUE] is None + + +@pytest.mark.parametrize('entity_id, attrs', [ + ('script.test', {}), + ('script.test', {ATTR_CAN_CANCEL: False}), +]) +async def test_reset_switch(hass, hk_driver, entity_id, attrs, events): + """Test if switch accessory is reset correctly.""" + domain = split_entity_id(entity_id)[0] + + hass.states.async_set(entity_id, None, attrs) + await hass.async_block_till_done() + acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.activate_only is True + assert acc.char_on.value is False + + call_turn_on = async_mock_service(hass, domain, 'turn_on') + call_turn_off = async_mock_service(hass, domain, 'turn_off') + + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert acc.char_on.value is True + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert acc.char_on.value is False + assert len(events) == 1 + assert not call_turn_off + + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert acc.char_on.value is False + assert len(events) == 1 + + +async def test_reset_switch_reload(hass, hk_driver, events): + """Test reset switch after script reload.""" + entity_id = 'script.test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.activate_only is True + + hass.states.async_set(entity_id, None, {ATTR_CAN_CANCEL: True}) + await hass.async_block_till_done() + assert acc.activate_only is False + + hass.states.async_set(entity_id, None, {ATTR_CAN_CANCEL: False}) + await hass.async_block_till_done() + assert acc.activate_only is True