mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Add more HomeKit models for discovery (#24391)
* Add more HomeKit models for discovery * Discover Tradfri with HomeKit * Add Wemo device info * Allow full match for HomeKit model * Fix tests
This commit is contained in:
parent
b30f4b8fc0
commit
0dc0706eb2
@ -13,9 +13,7 @@ from .connection import get_bridge_information, get_accessory_name
|
|||||||
|
|
||||||
|
|
||||||
HOMEKIT_IGNORE = [
|
HOMEKIT_IGNORE = [
|
||||||
'BSB002',
|
|
||||||
'Home Assistant Bridge',
|
'Home Assistant Bridge',
|
||||||
'TRADFRI gateway',
|
|
||||||
]
|
]
|
||||||
HOMEKIT_DIR = '.homekit'
|
HOMEKIT_DIR = '.homekit'
|
||||||
PAIRING_FILE = 'pairing.json'
|
PAIRING_FILE = 'pairing.json'
|
||||||
|
@ -175,6 +175,22 @@ class HueFlowHandler(config_entries.ConfigFlow):
|
|||||||
'path': 'phue-{}.conf'.format(serial)
|
'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):
|
async def async_step_import(self, import_info):
|
||||||
"""Import a new bridge as a config entry.
|
"""Import a new bridge as a config entry.
|
||||||
|
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
"Royal Philips Electronics"
|
"Royal Philips Electronics"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"homekit": {
|
||||||
|
"models": [
|
||||||
|
"BSB002"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@balloob"
|
"@balloob"
|
||||||
|
@ -87,6 +87,8 @@ class FlowHandler(config_entries.ConfigFlow):
|
|||||||
self._host = user_input['host']
|
self._host = user_input['host']
|
||||||
return await self.async_step_auth()
|
return await self.async_step_auth()
|
||||||
|
|
||||||
|
async_step_homekit = async_step_zeroconf
|
||||||
|
|
||||||
async def async_step_import(self, user_input):
|
async def async_step_import(self, user_input):
|
||||||
"""Import a config entry."""
|
"""Import a config entry."""
|
||||||
for entry in self._async_current_entries():
|
for entry in self._async_current_entries():
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"pytradfri[async]==6.0.1"
|
"pytradfri[async]==6.0.1"
|
||||||
],
|
],
|
||||||
|
"homekit": {
|
||||||
|
"models": [
|
||||||
|
"TRADFRI"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"zeroconf": ["_coap._udp.local."],
|
"zeroconf": ["_coap._udp.local."],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
"Belkin International Inc."
|
"Belkin International Inc."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"homekit": {
|
||||||
|
"models": [
|
||||||
|
"Wemo"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@sqldiablo"
|
"@sqldiablo"
|
||||||
|
@ -12,7 +12,7 @@ from homeassistant.util import convert
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN)
|
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)
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
|
|
||||||
@ -93,6 +93,14 @@ class WemoSwitch(SwitchDevice):
|
|||||||
"""Return the name of the switch if any."""
|
"""Return the name of the switch if any."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info."""
|
||||||
|
return {
|
||||||
|
'name': self._name,
|
||||||
|
'identifiers': {(WEMO_DOMAIN, self._serialnumber)},
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes of the device."""
|
"""Return the state attributes of the device."""
|
||||||
|
@ -112,7 +112,7 @@ def handle_homekit(hass, info) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
for test_model in HOMEKIT:
|
for test_model in HOMEKIT:
|
||||||
if not model.startswith(test_model):
|
if model != test_model and not model.startswith(test_model + " "):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
hass.add_job(
|
hass.add_job(
|
||||||
|
@ -20,5 +20,8 @@ ZEROCONF = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HOMEKIT = {
|
HOMEKIT = {
|
||||||
"LIFX ": "lifx"
|
"BSB002": "hue",
|
||||||
|
"LIFX": "lifx",
|
||||||
|
"TRADFRI": "tradfri",
|
||||||
|
"Wemo": "wemo"
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ def generate_and_validate(integrations: Dict[str, Integration]):
|
|||||||
try:
|
try:
|
||||||
with open(str(integration.path / "config_flow.py")) as fp:
|
with open(str(integration.path / "config_flow.py")) as fp:
|
||||||
content = fp.read()
|
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):
|
'register_discovery_flow' not in content):
|
||||||
integration.add_error(
|
integration.add_error(
|
||||||
'ssdp', 'Config flow has no async_step_ssdp')
|
'ssdp', 'Config flow has no async_step_ssdp')
|
||||||
|
@ -42,13 +42,13 @@ def generate_and_validate(integrations: Dict[str, Integration]):
|
|||||||
uses_discovery_flow = 'register_discovery_flow' in content
|
uses_discovery_flow = 'register_discovery_flow' in content
|
||||||
|
|
||||||
if (service_types and not uses_discovery_flow and
|
if (service_types and not uses_discovery_flow and
|
||||||
' async_step_zeroconf(' not in content):
|
' async_step_zeroconf' not in content):
|
||||||
integration.add_error(
|
integration.add_error(
|
||||||
'zeroconf', 'Config flow has no async_step_zeroconf')
|
'zeroconf', 'Config flow has no async_step_zeroconf')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (homekit_models and not uses_discovery_flow and
|
if (homekit_models and not uses_discovery_flow and
|
||||||
' async_step_homekit(' not in content):
|
' async_step_homekit' not in content):
|
||||||
integration.add_error(
|
integration.add_error(
|
||||||
'zeroconf', 'Config flow has no async_step_homekit')
|
'zeroconf', 'Config flow has no async_step_homekit')
|
||||||
continue
|
continue
|
||||||
@ -64,9 +64,6 @@ def generate_and_validate(integrations: Dict[str, Integration]):
|
|||||||
service_type_dict[service_type].append(domain)
|
service_type_dict[service_type].append(domain)
|
||||||
|
|
||||||
for model in homekit_models:
|
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:
|
if model in homekit_dict:
|
||||||
integration.add_error(
|
integration.add_error(
|
||||||
'zeroconf',
|
'zeroconf',
|
||||||
|
@ -283,7 +283,7 @@ async def test_discovery_ignored_model(hass):
|
|||||||
'host': '127.0.0.1',
|
'host': '127.0.0.1',
|
||||||
'port': 8080,
|
'port': 8080,
|
||||||
'properties': {
|
'properties': {
|
||||||
'md': 'BSB002',
|
'md': config_flow.HOMEKIT_IGNORE[0],
|
||||||
'id': '00:00:00:00:00:00',
|
'id': '00:00:00:00:00:00',
|
||||||
'c#': 1,
|
'c#': 1,
|
||||||
'sf': 1,
|
'sf': 1,
|
||||||
|
@ -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
|
# We did not process the result of this entry but already removed the old
|
||||||
# ones. So we should have 0 entries.
|
# ones. So we should have 0 entries.
|
||||||
assert len(hass.config_entries.async_entries('hue')) == 0
|
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'
|
||||||
|
@ -31,12 +31,15 @@ def get_service_info_mock(service_type, name):
|
|||||||
properties={b'macaddress': b'ABCDEF012345'})
|
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 homekit info for get_service_info."""
|
||||||
return ServiceInfo(
|
def mock_homekit_info(service_type, name):
|
||||||
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
|
return ServiceInfo(
|
||||||
priority=0, server='name.local.',
|
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
|
||||||
properties={b'md': b'LIFX Bulb'})
|
priority=0, server='name.local.',
|
||||||
|
properties={b'md': model.encode()})
|
||||||
|
|
||||||
|
return mock_homekit_info
|
||||||
|
|
||||||
|
|
||||||
async def test_setup(hass, mock_zeroconf):
|
async def test_setup(hass, mock_zeroconf):
|
||||||
@ -54,7 +57,7 @@ async def test_setup(hass, mock_zeroconf):
|
|||||||
assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2
|
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."""
|
"""Test configured options for a device are loaded via config entry."""
|
||||||
with patch.dict(
|
with patch.dict(
|
||||||
zc_gen.ZEROCONF, {
|
zc_gen.ZEROCONF, {
|
||||||
@ -65,10 +68,32 @@ async def test_homekit(hass, mock_zeroconf):
|
|||||||
) as mock_config_flow, patch.object(
|
) as mock_config_flow, patch.object(
|
||||||
zeroconf, 'ServiceBrowser', side_effect=service_update_mock
|
zeroconf, 'ServiceBrowser', side_effect=service_update_mock
|
||||||
) as mock_service_browser:
|
) 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(
|
assert await async_setup_component(
|
||||||
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||||
|
|
||||||
assert len(mock_service_browser.mock_calls) == 1
|
assert len(mock_service_browser.mock_calls) == 1
|
||||||
assert len(mock_config_flow.mock_calls) == 2
|
assert len(mock_config_flow.mock_calls) == 2
|
||||||
assert mock_config_flow.mock_calls[0][1][0] == 'lifx'
|
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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user