Skip to content

Commit

Permalink
Add more HomeKit models for discovery (home-assistant#24391)
Browse files Browse the repository at this point in the history
* Add more HomeKit models for discovery

* Discover Tradfri with HomeKit

* Add Wemo device info

* Allow full match for HomeKit model

* Fix tests
  • Loading branch information
balloob authored Jun 8, 2019
1 parent b30f4b8 commit 0dc0706
Show file tree
Hide file tree
Showing 14 changed files with 118 additions and 19 deletions.
2 changes: 0 additions & 2 deletions homeassistant/components/homekit_controller/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@


HOMEKIT_IGNORE = [
'BSB002',
'Home Assistant Bridge',
'TRADFRI gateway',
]
HOMEKIT_DIR = '.homekit'
PAIRING_FILE = 'pairing.json'
Expand Down
16 changes: 16 additions & 0 deletions homeassistant/components/hue/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,22 @@ async def async_step_ssdp(self, discovery_info):
'path': 'phue-{}.conf'.format(serial)
})

async def async_step_homekit(self, homekit_info):
"""Handle HomeKit discovery."""
# pylint: disable=unsupported-assignment-operation
host = self.context['host'] = homekit_info.get('host')

if any(host == flow['context']['host']
for flow in self._async_in_progress()):
return self.async_abort(reason='already_in_progress')

if host in configured_hosts(self.hass):
return self.async_abort(reason='already_configured')

return await self.async_step_import({
'host': host,
})

async def async_step_import(self, import_info):
"""Import a new bridge as a config entry.
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/hue/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"Royal Philips Electronics"
]
},
"homekit": {
"models": [
"BSB002"
]
},
"dependencies": [],
"codeowners": [
"@balloob"
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/tradfri/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ async def async_step_zeroconf(self, user_input):
self._host = user_input['host']
return await self.async_step_auth()

async_step_homekit = async_step_zeroconf

async def async_step_import(self, user_input):
"""Import a config entry."""
for entry in self._async_current_entries():
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/tradfri/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"requirements": [
"pytradfri[async]==6.0.1"
],
"homekit": {
"models": [
"TRADFRI"
]
},
"dependencies": [],
"zeroconf": ["_coap._udp.local."],
"codeowners": [
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/wemo/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"Belkin International Inc."
]
},
"homekit": {
"models": [
"Wemo"
]
},
"dependencies": [],
"codeowners": [
"@sqldiablo"
Expand Down
10 changes: 9 additions & 1 deletion homeassistant/components/wemo/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from homeassistant.const import (
STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN)

from . import SUBSCRIPTION_REGISTRY
from . import SUBSCRIPTION_REGISTRY, DOMAIN as WEMO_DOMAIN

SCAN_INTERVAL = timedelta(seconds=10)

Expand Down Expand Up @@ -93,6 +93,14 @@ def name(self):
"""Return the name of the switch if any."""
return self._name

@property
def device_info(self):
"""Return the device info."""
return {
'name': self._name,
'identifiers': {(WEMO_DOMAIN, self._serialnumber)},
}

@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/zeroconf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def handle_homekit(hass, info) -> bool:
return False

for test_model in HOMEKIT:
if not model.startswith(test_model):
if model != test_model and not model.startswith(test_model + " "):
continue

hass.add_job(
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/generated/zeroconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
}

HOMEKIT = {
"LIFX ": "lifx"
"BSB002": "hue",
"LIFX": "lifx",
"TRADFRI": "tradfri",
"Wemo": "wemo"
}
2 changes: 1 addition & 1 deletion script/hassfest/ssdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
try:
with open(str(integration.path / "config_flow.py")) as fp:
content = fp.read()
if (' async_step_ssdp(' not in content and
if (' async_step_ssdp' not in content and
'register_discovery_flow' not in content):
integration.add_error(
'ssdp', 'Config flow has no async_step_ssdp')
Expand Down
7 changes: 2 additions & 5 deletions script/hassfest/zeroconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ def generate_and_validate(integrations: Dict[str, Integration]):
uses_discovery_flow = 'register_discovery_flow' in content

if (service_types and not uses_discovery_flow and
' async_step_zeroconf(' not in content):
' async_step_zeroconf' not in content):
integration.add_error(
'zeroconf', 'Config flow has no async_step_zeroconf')
continue

if (homekit_models and not uses_discovery_flow and
' async_step_homekit(' not in content):
' async_step_homekit' not in content):
integration.add_error(
'zeroconf', 'Config flow has no async_step_homekit')
continue
Expand All @@ -64,9 +64,6 @@ def generate_and_validate(integrations: Dict[str, Integration]):
service_type_dict[service_type].append(domain)

for model in homekit_models:
# We add a space, as we want to test for it to be model + space.
model += " "

if model in homekit_dict:
integration.add_error(
'zeroconf',
Expand Down
2 changes: 1 addition & 1 deletion tests/components/homekit_controller/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ async def test_discovery_ignored_model(hass):
'host': '127.0.0.1',
'port': 8080,
'properties': {
'md': 'BSB002',
'md': config_flow.HOMEKIT_IGNORE[0],
'id': '00:00:00:00:00:00',
'c#': 1,
'sf': 1,
Expand Down
35 changes: 35 additions & 0 deletions tests/components/hue/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,38 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass):
# We did not process the result of this entry but already removed the old
# ones. So we should have 0 entries.
assert len(hass.config_entries.async_entries('hue')) == 0


async def test_bridge_homekit(hass):
"""Test a bridge being discovered via HomeKit."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
flow.context = {}

with patch.object(config_flow, 'get_bridge',
side_effect=errors.AuthenticationRequired):
result = await flow.async_step_homekit({
'host': '0.0.0.0',
'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})

assert result['type'] == 'form'
assert result['step_id'] == 'link'


async def test_bridge_homekit_already_configured(hass):
"""Test if a HomeKit discovered bridge has already been configured."""
MockConfigEntry(domain='hue', data={
'host': '0.0.0.0'
}).add_to_hass(hass)

flow = config_flow.HueFlowHandler()
flow.hass = hass
flow.context = {}

result = await flow.async_step_homekit({
'host': '0.0.0.0',
})

assert result['type'] == 'abort'
39 changes: 32 additions & 7 deletions tests/components/zeroconf/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ def get_service_info_mock(service_type, name):
properties={b'macaddress': b'ABCDEF012345'})


def get_homekit_info_mock(service_type, name):
def get_homekit_info_mock(model):
"""Return homekit info for get_service_info."""
return ServiceInfo(
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
priority=0, server='name.local.',
properties={b'md': b'LIFX Bulb'})
def mock_homekit_info(service_type, name):
return ServiceInfo(
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
priority=0, server='name.local.',
properties={b'md': model.encode()})

return mock_homekit_info


async def test_setup(hass, mock_zeroconf):
Expand All @@ -54,7 +57,7 @@ async def test_setup(hass, mock_zeroconf):
assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2


async def test_homekit(hass, mock_zeroconf):
async def test_homekit_match_partial(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF, {
Expand All @@ -65,10 +68,32 @@ async def test_homekit(hass, mock_zeroconf):
) as mock_config_flow, patch.object(
zeroconf, 'ServiceBrowser', side_effect=service_update_mock
) as mock_service_browser:
mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock
mock_zeroconf.get_service_info.side_effect = \
get_homekit_info_mock("LIFX bulb")
assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})

assert len(mock_service_browser.mock_calls) == 1
assert len(mock_config_flow.mock_calls) == 2
assert mock_config_flow.mock_calls[0][1][0] == 'lifx'


async def test_homekit_match_full(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
with patch.dict(
zc_gen.ZEROCONF, {
zeroconf.HOMEKIT_TYPE: ["homekit_controller"]
}, clear=True
), patch.object(
hass.config_entries, 'flow'
) as mock_config_flow, patch.object(
zeroconf, 'ServiceBrowser', side_effect=service_update_mock
) as mock_service_browser:
mock_zeroconf.get_service_info.side_effect = \
get_homekit_info_mock("BSB002")
assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})

assert len(mock_service_browser.mock_calls) == 1
assert len(mock_config_flow.mock_calls) == 2
assert mock_config_flow.mock_calls[0][1][0] == 'hue'

0 comments on commit 0dc0706

Please sign in to comment.