Skip to content
Merged
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
29 changes: 29 additions & 0 deletions homeassistant/components/homekit/type_switches.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD,
CATEGORY_SPRINKLER, 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
Expand Down Expand Up @@ -71,21 +73,48 @@ def __init__(self, *args):
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scene support could be added here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can see in my comment above, I already added it. Works fine too


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',
Expand Down
89 changes: 78 additions & 11 deletions tests/components/homekit/test_type_switches.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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