Skip to content

Commit

Permalink
Add support for HAP events, making homekit_controller a local push in…
Browse files Browse the repository at this point in the history
…tegration.
  • Loading branch information
Jc2k committed Feb 26, 2020
1 parent 5d31f95 commit 9c9ea6b
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 2 deletions.
7 changes: 7 additions & 0 deletions homeassistant/components/homekit_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ async def async_added_to_hass(self):
)

self._accessory.add_pollable_characteristics(self.pollable_characteristics)
self._accessory.add_watchable_characteristics(self.watchable_characteristics)

async def async_will_remove_from_hass(self):
"""Prepare to be removed from hass."""
self._accessory.remove_pollable_characteristics(self._aid)
self._accessory.remove_watchable_characteristics(self._aid)

for signal_remove in self._signals:
signal_remove()
Expand All @@ -71,6 +73,7 @@ def setup(self):
characteristic_types = [get_uuid(c) for c in self.get_characteristic_types()]

self.pollable_characteristics = []
self.watchable_characteristics = []
self._chars = {}
self._char_names = {}

Expand Down Expand Up @@ -98,6 +101,10 @@ def _setup_characteristic(self, char):
if "pr" in char["perms"]:
self.pollable_characteristics.append((self._aid, char["iid"]))

# Build up a list of (aid, iid) tuples to subscribe to
if "ev" in char["perms"]:
self.watchable_characteristics.append((self._aid, char["iid"]))

# Build a map of ctype -> iid
short_name = CharacteristicsTypes.get_short(char["type"])
self._chars[short_name] = char["iid"]
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
"""Handle a HomeKit config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

def __init__(self):
"""Initialize the homekit_controller flow."""
Expand Down
20 changes: 20 additions & 0 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def __init__(self, hass, config_entry, pairing_data):
self._polling_lock = asyncio.Lock()
self._polling_lock_warned = False

self.watchable_characteristics = []

self.pairing.dispatcher_connect(self.process_new_events)

def add_pollable_characteristics(self, characteristics):
"""Add (aid, iid) pairs that we need to poll."""
self.pollable_characteristics.extend(characteristics)
Expand All @@ -118,6 +122,17 @@ def remove_pollable_characteristics(self, accessory_id):
char for char in self.pollable_characteristics if char[0] != accessory_id
]

def add_watchable_characteristics(self, characteristics):
"""Add (aid, iid) pairs that we need to poll."""
self.watchable_characteristics.extend(characteristics)
self.hass.add_job(self.pairing.subscribe(characteristics))

def remove_watchable_characteristics(self, accessory_id):
"""Remove all pollable characteristics by accessory id."""
self.watchable_characteristics = [
char for char in self.watchable_characteristics if char[0] != accessory_id
]

@callback
def async_set_unavailable(self):
"""Mark state of all entities on this connection as unavailable."""
Expand Down Expand Up @@ -163,6 +178,9 @@ async def async_process_entity_map(self):

self.add_entities()

if self.watchable_characteristics:
await self.pairing.subscribe(self.watchable_characteristics)

await self.async_update()

return True
Expand All @@ -172,6 +190,8 @@ async def async_unload(self):
if self._polling_interval_remover:
self._polling_interval_remover()

await self.pairing.unsubscribe(self.watchable_characteristics)

unloads = []
for platform in self.platforms:
unloads.append(
Expand Down
5 changes: 5 additions & 0 deletions tests/components/homekit_controller/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def __init__(self, hass, entity_id, pairing, accessory, config_entry):
char_name = CharacteristicsTypes.get_short(char.type)
self.characteristics[(service_name, char_name)] = char

async def update_named_service(self, service, characteristics):
"""Update a service."""
self.pairing.testing.update_named_service(service, characteristics)
await self.hass.async_block_till_done()

async def poll_and_get_state(self):
"""Trigger a time based poll and return the current entity state."""
await time_changed(self.hass, 60)
Expand Down
59 changes: 58 additions & 1 deletion tests/components/homekit_controller/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

from tests.components.homekit_controller.common import setup_test_component

LIGHT_BULB_NAME = "Light Bulb"
LIGHT_BULB_ENTITY_ID = "light.testdevice"

LIGHT_ON = ("lightbulb", "on")
LIGHT_BRIGHTNESS = ("lightbulb", "brightness")
LIGHT_HUE = ("lightbulb", "hue")
Expand All @@ -15,7 +18,7 @@

def create_lightbulb_service(accessory):
"""Define lightbulb characteristics."""
service = accessory.add_service(ServicesTypes.LIGHTBULB)
service = accessory.add_service(ServicesTypes.LIGHTBULB, name=LIGHT_BULB_NAME)

on_char = service.add_char(CharacteristicsTypes.ON)
on_char.value = 0
Expand Down Expand Up @@ -110,6 +113,37 @@ async def test_switch_read_light_state(hass, utcnow):
assert state.state == "off"


async def test_switch_push_light_state(hass, utcnow):
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(hass, create_lightbulb_service_with_hs)

# Initial state is that the light is off
state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "off"

await helper.update_named_service(
LIGHT_BULB_NAME,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.HUE: 4,
CharacteristicsTypes.SATURATION: 5,
},
)

state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["hs_color"] == (4, 5)

# Simulate that device switched off in the real world not via HA
await helper.update_named_service(
LIGHT_BULB_NAME, {CharacteristicsTypes.ON: False}
)
state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "off"


async def test_switch_read_light_state_color_temp(hass, utcnow):
"""Test that we can read the color_temp of a light accessory."""
helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp)
Expand All @@ -129,6 +163,29 @@ async def test_switch_read_light_state_color_temp(hass, utcnow):
assert state.attributes["color_temp"] == 400


async def test_switch_push_light_state_color_temp(hass, utcnow):
"""Test that we can read the state of a HomeKit light accessory."""
helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp)

# Initial state is that the light is off
state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "off"

await helper.update_named_service(
LIGHT_BULB_NAME,
{
CharacteristicsTypes.ON: True,
CharacteristicsTypes.BRIGHTNESS: 100,
CharacteristicsTypes.COLOR_TEMPERATURE: 400,
},
)

state = hass.states.get(LIGHT_BULB_ENTITY_ID)
assert state.state == "on"
assert state.attributes["brightness"] == 255
assert state.attributes["color_temp"] == 400


async def test_light_becomes_unavailable_but_recovers(hass, utcnow):
"""Test transition to and from unavailable state."""
helper = await setup_test_component(hass, create_lightbulb_service_with_color_temp)
Expand Down

0 comments on commit 9c9ea6b

Please sign in to comment.