From 798b72e1642f754848cc644f511093a7c4b29306 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Jun 2019 10:06:53 -0700 Subject: [PATCH 01/10] Add a discovery config flow to Wemo (#24208) --- .../components/discovery/__init__.py | 22 ++++++++-------- homeassistant/components/wemo/__init__.py | 25 +++++++++++++++---- homeassistant/components/wemo/config_flow.py | 15 +++++++++++ homeassistant/components/wemo/manifest.json | 6 +++++ homeassistant/components/wemo/strings.json | 15 +++++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 3 +++ script/hassfest/ssdp.py | 4 ++- 8 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/wemo/config_flow.py create mode 100644 homeassistant/components/wemo/strings.json diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 0541b5d223a..a7c306ad241 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -58,7 +58,6 @@ SERVICE_HANDLERS = { SERVICE_MOBILE_APP: ('mobile_app', None), SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), - SERVICE_WEMO: ('wemo', None), SERVICE_HASSIO: ('hassio', None), SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_ENIGMA2: ('media_player', 'enigma2'), @@ -94,19 +93,20 @@ OPTIONAL_SERVICE_HANDLERS = { SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), } -MIGRATED_SERVICE_HANDLERS = { - 'axis': None, - 'deconz': None, - 'esphome': None, - 'ikea_tradfri': None, - 'homekit': None, - 'philips_hue': None -} +MIGRATED_SERVICE_HANDLERS = [ + 'axis', + 'deconz', + 'esphome', + 'ikea_tradfri', + 'homekit', + 'philips_hue', + SERVICE_WEMO, +] DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \ - list(MIGRATED_SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \ - list(MIGRATED_SERVICE_HANDLERS) + MIGRATED_SERVICE_HANDLERS CONF_IGNORE = 'ignore' CONF_ENABLE = 'enable' diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index d921075bc1a..8353b52b9f0 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -4,6 +4,7 @@ import logging import requests import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_WEMO from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery @@ -68,22 +69,35 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up for WeMo devices.""" + hass.data[DOMAIN] = config + + if DOMAIN in config: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a wemo config entry.""" import pywemo + config = hass.data[DOMAIN] + # Keep track of WeMo devices devices = [] # Keep track of WeMo device subscriptions for push updates global SUBSCRIPTION_REGISTRY SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() - SUBSCRIPTION_REGISTRY.start() + await hass.async_add_executor_job(SUBSCRIPTION_REGISTRY.start) def stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") SUBSCRIPTION_REGISTRY.stop() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) def setup_url_for_device(device): """Determine setup.xml url for given device.""" @@ -119,7 +133,7 @@ def setup(hass, config): discovery.load_platform( hass, component, DOMAIN, discovery_info, config) - discovery.listen(hass, SERVICE_WEMO, discovery_dispatch) + discovery.async_listen(hass, SERVICE_WEMO, discovery_dispatch) def discover_wemo_devices(now): """Run discovery for WeMo devices.""" @@ -145,7 +159,7 @@ def setup(hass, config): if d[1].serialnumber == device.serialnumber]: devices.append((url, device)) - if config.get(DOMAIN, {}).get(CONF_DISCOVERY): + if config.get(DOMAIN, {}).get(CONF_DISCOVERY, DEFAULT_DISCOVERY): _LOGGER.debug("Scanning network for WeMo devices...") for device in pywemo.discover_devices(): if not [d[1] for d in devices @@ -168,6 +182,7 @@ def setup(hass, config): _LOGGER.debug("WeMo device discovery has finished") - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, discover_wemo_devices) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, discover_wemo_devices) return True diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py new file mode 100644 index 00000000000..61094dbab32 --- /dev/null +++ b/homeassistant/components/wemo/config_flow.py @@ -0,0 +1,15 @@ +"""Config flow for Wemo.""" +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from . import DOMAIN + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + import pywemo + + return bool(pywemo.discover_devices()) + + +config_entry_flow.register_discovery_flow( + DOMAIN, 'Wemo', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 238be891886..c610c28da39 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -1,10 +1,16 @@ { "domain": "wemo", "name": "Wemo", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/wemo", "requirements": [ "pywemo==0.4.34" ], + "ssdp": { + "manufacturer": [ + "Belkin International Inc." + ] + }, "dependencies": [], "codeowners": [ "@sqldiablo" diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json new file mode 100644 index 00000000000..d4b40817cb3 --- /dev/null +++ b/homeassistant/components/wemo/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "Wemo", + "step": { + "confirm": { + "title": "Wemo", + "description": "Do you want to set up Wemo?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Wemo is possible.", + "no_devices_found": "No Wemo devices found on the network." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c9a8c593b27..87da17434b9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -49,6 +49,7 @@ FLOWS = [ "twilio", "unifi", "upnp", + "wemo", "zha", "zone", "zwave" diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index cc1d286bf5f..4da9f41a203 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -7,6 +7,9 @@ To update, run python3 -m hassfest SSDP = { "device_type": {}, "manufacturer": { + "Belkin International Inc.": [ + "wemo" + ], "Royal Philips Electronics": [ "deconz", "hue" diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index b5c4b9721c0..d9a50dd7c38 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -43,7 +43,9 @@ def generate_and_validate(integrations: Dict[str, Integration]): try: with open(str(integration.path / "config_flow.py")) as fp: - if ' async_step_ssdp(' not in fp.read(): + content = fp.read() + 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') continue From ebc09017b8631409da160e4f927d1bde3afdb505 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Jun 2019 03:07:30 -0700 Subject: [PATCH 02/10] Initiate websession inside event loop (#24331) --- homeassistant/components/tado/device_tracker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 3bb62f328b9..31b424b9cd4 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -42,6 +42,7 @@ class TadoDeviceScanner(DeviceScanner): def __init__(self, hass, config): """Initialize the scanner.""" + self.hass = hass self.last_results = [] self.username = config[CONF_USERNAME] @@ -60,8 +61,7 @@ class TadoDeviceScanner(DeviceScanner): # The API URL always needs a username and password self.tadoapiurl += '?username={username}&password={password}' - self.websession = async_create_clientsession( - hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + self.websession = None self.success_init = asyncio.run_coroutine_threadsafe( self._async_update_info(), hass.loop @@ -92,6 +92,10 @@ class TadoDeviceScanner(DeviceScanner): """ _LOGGER.debug("Requesting Tado") + if self.websession is None: + self.websession = async_create_clientsession( + self.hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + last_results = [] try: From b8e20fcadf053b14c5c07b9f3e5577a26317a5a7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 7 Jun 2019 21:22:02 +0200 Subject: [PATCH 03/10] Bump dependency (#24376) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index dc64e90ba9a..2b1bef9081e 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/components/axis", - "requirements": ["axis==24"], + "requirements": ["axis==25"], "dependencies": [], "zeroconf": ["_axis-video._tcp.local."], "codeowners": ["@kane610"] diff --git a/requirements_all.txt b/requirements_all.txt index debfb43b41e..9b135d7110a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -212,7 +212,7 @@ av==6.1.2 # avion==0.10 # homeassistant.components.axis -axis==24 +axis==25 # homeassistant.components.azure_event_hub azure-eventhub==1.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd76ca3f748..18562f53554 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ apns2==0.3.0 av==6.1.2 # homeassistant.components.axis -axis==24 +axis==25 # homeassistant.components.zha bellows-homeassistant==0.7.3 From d1b82e9edec80f55d5d439db40690db614e44533 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 7 Jun 2019 23:29:31 -0400 Subject: [PATCH 04/10] Updated pubnubsub-handler to 1.0.7 to fix crash on slow startup (#24388) --- homeassistant/components/wink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index 118f7a19733..a878b084169 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -3,7 +3,7 @@ "name": "Wink", "documentation": "https://www.home-assistant.io/components/wink", "requirements": [ - "pubnubsub-handler==1.0.6", + "pubnubsub-handler==1.0.7", "python-wink==1.10.5" ], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index 9b135d7110a..8b635326058 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -918,7 +918,7 @@ psutil==5.6.2 ptvsd==4.2.8 # homeassistant.components.wink -pubnubsub-handler==1.0.6 +pubnubsub-handler==1.0.7 # homeassistant.components.pushbullet pushbullet.py==0.11.0 From 2fed0163474e50f83417a2fb6e74b9d6f79ec375 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Jun 2019 23:08:22 -0700 Subject: [PATCH 05/10] Fix automation failing to restore state (#24390) * Fix automation off * Fix tests --- .../components/automation/__init__.py | 71 +++++++++++-------- .../automation/test_homeassistant.py | 4 +- tests/components/automation/test_init.py | 22 +++--- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index beca5cd236c..90b5857b13c 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -190,6 +190,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._last_triggered = None self._hidden = hidden self._initial_state = initial_state + self._is_enabled = False @property def name(self): @@ -216,7 +217,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): @property def is_on(self) -> bool: """Return True if entity is on.""" - return self._async_detach_triggers is not None + return (self._async_detach_triggers is not None or + self._is_enabled) async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" @@ -239,37 +241,16 @@ class AutomationEntity(ToggleEntity, RestoreEntity): "initial state", self.entity_id, enable_automation) - if not enable_automation: - return - - # HomeAssistant is starting up - if self.hass.state == CoreState.not_running: - async def async_enable_automation(event): - """Start automation on startup.""" - await self.async_enable() - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_enable_automation) - - # HomeAssistant is running - else: + if enable_automation: await self.async_enable() async def async_turn_on(self, **kwargs) -> None: """Turn the entity on and update the state.""" - if self.is_on: - return - await self.async_enable() async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" - if not self.is_on: - return - - self._async_detach_triggers() - self._async_detach_triggers = None - await self.async_update_ha_state() + await self.async_disable() async def async_trigger(self, variables, skip_condition=False, context=None): @@ -296,19 +277,51 @@ class AutomationEntity(ToggleEntity, RestoreEntity): async def async_will_remove_from_hass(self): """Remove listeners when removing automation from HASS.""" await super().async_will_remove_from_hass() - await self.async_turn_off() + await self.async_disable() async def async_enable(self): """Enable this automation entity. This method is a coroutine. """ - if self.is_on: + if self._is_enabled: return - self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger) - await self.async_update_ha_state() + self._is_enabled = True + + # HomeAssistant is starting up + if self.hass.state != CoreState.not_running: + self._async_detach_triggers = await self._async_attach_triggers( + self.async_trigger) + self.async_write_ha_state() + return + + async def async_enable_automation(event): + """Start automation on startup.""" + # Don't do anything if no longer enabled or already attached + if (not self._is_enabled or + self._async_detach_triggers is not None): + return + + self._async_detach_triggers = await self._async_attach_triggers( + self.async_trigger) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_enable_automation) + self.async_write_ha_state() + + async def async_disable(self): + """Disable the automation entity.""" + if not self._is_enabled: + return + + self._is_enabled = False + + if self._async_detach_triggers is not None: + self._async_detach_triggers() + self._async_detach_triggers = None + + self.async_write_ha_state() @property def device_state_attributes(self): diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index d9cb5313c3e..742a2aa857c 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -29,7 +29,7 @@ def test_if_fires_on_hass_start(hass): res = yield from async_setup_component(hass, automation.DOMAIN, config) assert res - assert not automation.is_on(hass, 'automation.hello') + assert automation.is_on(hass, 'automation.hello') assert len(calls) == 0 yield from hass.async_start() @@ -64,7 +64,7 @@ def test_if_fires_on_hass_shutdown(hass): } }) assert res - assert not automation.is_on(hass, 'automation.hello') + assert automation.is_on(hass, 'automation.hello') assert len(calls) == 0 yield from hass.async_start() diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 179c5f84895..81d7a8b257f 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -696,12 +696,12 @@ def test_initial_value_off(hass): assert len(calls) == 0 -@asyncio.coroutine -def test_initial_value_on(hass): +async def test_initial_value_on(hass): """Test initial value on.""" + hass.state = CoreState.not_running calls = async_mock_service(hass, 'test', 'automation') - res = yield from async_setup_component(hass, automation.DOMAIN, { + assert await async_setup_component(hass, automation.DOMAIN, { automation.DOMAIN: { 'alias': 'hello', 'initial_state': 'on', @@ -715,23 +715,23 @@ def test_initial_value_on(hass): } } }) - assert res assert automation.is_on(hass, 'automation.hello') + await hass.async_start() hass.bus.async_fire('test_event') - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 -@asyncio.coroutine -def test_initial_value_off_but_restore_on(hass): +async def test_initial_value_off_but_restore_on(hass): """Test initial value off and restored state is turned on.""" + hass.state = CoreState.not_running calls = async_mock_service(hass, 'test', 'automation') mock_restore_cache(hass, ( State('automation.hello', STATE_ON), )) - res = yield from async_setup_component(hass, automation.DOMAIN, { + await async_setup_component(hass, automation.DOMAIN, { automation.DOMAIN: { 'alias': 'hello', 'initial_state': 'off', @@ -745,11 +745,11 @@ def test_initial_value_off_but_restore_on(hass): } } }) - assert res assert not automation.is_on(hass, 'automation.hello') + await hass.async_start() hass.bus.async_fire('test_event') - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 0 @@ -858,7 +858,7 @@ def test_automation_not_trigger_on_bootstrap(hass): } }) assert res - assert not automation.is_on(hass, 'automation.hello') + assert automation.is_on(hass, 'automation.hello') hass.bus.async_fire('test_event') yield from hass.async_block_till_done() From 7d9988fd753132f153c172ddc08e9f7fe00a6106 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Jun 2019 22:59:51 -0700 Subject: [PATCH 06/10] 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 --- .../homekit_controller/config_flow.py | 2 - homeassistant/components/hue/config_flow.py | 16 ++++++++ homeassistant/components/hue/manifest.json | 5 +++ .../components/tradfri/config_flow.py | 2 + .../components/tradfri/manifest.json | 5 +++ homeassistant/components/wemo/manifest.json | 5 +++ homeassistant/components/wemo/switch.py | 10 ++++- homeassistant/components/zeroconf/__init__.py | 2 +- homeassistant/generated/zeroconf.py | 5 ++- script/hassfest/ssdp.py | 2 +- script/hassfest/zeroconf.py | 7 +--- .../homekit_controller/test_config_flow.py | 2 +- tests/components/hue/test_config_flow.py | 35 +++++++++++++++++ tests/components/zeroconf/test_init.py | 39 +++++++++++++++---- 14 files changed, 118 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2ce8c0db6b7..9ddb144ec9a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -13,9 +13,7 @@ from .connection import get_bridge_information, get_accessory_name HOMEKIT_IGNORE = [ - 'BSB002', 'Home Assistant Bridge', - 'TRADFRI gateway', ] HOMEKIT_DIR = '.homekit' PAIRING_FILE = 'pairing.json' diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9c81d144d1c..d57706f7ac8 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -175,6 +175,22 @@ class HueFlowHandler(config_entries.ConfigFlow): '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. diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index d16988529b1..c0c7c462f90 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -11,6 +11,11 @@ "Royal Philips Electronics" ] }, + "homekit": { + "models": [ + "BSB002" + ] + }, "dependencies": [], "codeowners": [ "@balloob" diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 76f6a8f5764..bfabf4fd12a 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -87,6 +87,8 @@ class FlowHandler(config_entries.ConfigFlow): 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(): diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index aba3805a4aa..ba6b21e0028 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -6,6 +6,11 @@ "requirements": [ "pytradfri[async]==6.0.1" ], + "homekit": { + "models": [ + "TRADFRI" + ] + }, "dependencies": [], "zeroconf": ["_coap._udp.local."], "codeowners": [ diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index c610c28da39..1902df1060b 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -11,6 +11,11 @@ "Belkin International Inc." ] }, + "homekit": { + "models": [ + "Wemo" + ] + }, "dependencies": [], "codeowners": [ "@sqldiablo" diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index b8967cead3b..79f941d8bcf 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -12,7 +12,7 @@ from homeassistant.util import convert 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) @@ -93,6 +93,14 @@ class WemoSwitch(SwitchDevice): """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.""" diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 289aba6ef56..6011712c2f9 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -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( diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 024bb89dc99..4e46e9dd366 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -20,5 +20,8 @@ ZEROCONF = { } HOMEKIT = { - "LIFX ": "lifx" + "BSB002": "hue", + "LIFX": "lifx", + "TRADFRI": "tradfri", + "Wemo": "wemo" } diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index d9a50dd7c38..3c13da98a9b 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -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') diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 25e8da99b55..f30899d5948 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -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 @@ -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', diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index b5f923dd55e..99562f60045 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -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, diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index b7736e62390..a4524dfd48d 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -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' diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 27c1dc75749..e67d9063b0a 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -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): @@ -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, { @@ -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' From 14066dfb5ac20b1b2e375a6c47e43fdb68426d10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Jun 2019 23:08:55 -0700 Subject: [PATCH 07/10] Check cloud trusted proxies (#24395) --- homeassistant/components/cloud/const.py | 4 ++ homeassistant/components/cloud/http_api.py | 8 +++- homeassistant/components/cloud/prefs.py | 22 ++++++++- homeassistant/components/http/__init__.py | 1 + tests/components/cloud/test_http_api.py | 53 +++++++++++++++++++++- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index e2f4b9c0785..65062213a63 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -38,3 +38,7 @@ DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update' class InvalidTrustedNetworks(Exception): """Raised when invalid trusted networks config.""" + + +class InvalidTrustedProxies(Exception): + """Raised when invalid trusted proxies config.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index e6151a917af..9908268b252 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -18,7 +18,8 @@ from homeassistant.components.google_assistant import helpers as google_helpers from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, - PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks) + PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks, + InvalidTrustedProxies) _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,10 @@ SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ _CLOUD_ERRORS = { InvalidTrustedNetworks: (500, 'Remote UI not compatible with 127.0.0.1/::1' - ' as a trusted network.') + ' as a trusted network.'), + InvalidTrustedProxies: + (500, 'Remote UI not compatible with 127.0.0.1/::1' + ' as trusted proxies.'), } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0f45f25c49b..9f2579134e5 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -6,7 +6,7 @@ from .const import ( PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA, PREF_ALIASES, PREF_SHOULD_EXPOSE, - InvalidTrustedNetworks) + InvalidTrustedNetworks, InvalidTrustedProxies) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -59,6 +59,9 @@ class CloudPreferences: if remote_enabled is True and self._has_local_trusted_network: raise InvalidTrustedNetworks + if remote_enabled is True and self._has_local_trusted_proxies: + raise InvalidTrustedProxies + await self._store.async_save(self._prefs) async def async_update_google_entity_config( @@ -112,7 +115,7 @@ class CloudPreferences: if not enabled: return False - if self._has_local_trusted_network: + if self._has_local_trusted_network or self._has_local_trusted_proxies: return False return True @@ -162,3 +165,18 @@ class CloudPreferences: return True return False + + @property + def _has_local_trusted_proxies(self) -> bool: + """Return if we allow localhost to be a proxy and use its data.""" + if not hasattr(self._hass, 'http'): + return False + + local4 = ip_address('127.0.0.1') + local6 = ip_address('::1') + + if any(local4 in nwk or local6 in nwk + for nwk in self._hass.http.trusted_proxies): + return True + + return False diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ad64b38200a..a21fb2ab632 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -228,6 +228,7 @@ class HomeAssistantHTTP: self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port + self.trusted_proxies = trusted_proxies self.is_ban_enabled = is_ban_enabled self.ssl_profile = ssl_profile self._handler = None diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 5ccaba14be6..24bd647405a 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,6 +1,7 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from unittest.mock import patch, MagicMock +from ipaddress import ip_network import pytest from jose import jwt @@ -672,7 +673,7 @@ async def test_enabling_remote_trusted_networks_local6( async def test_enabling_remote_trusted_networks_other( hass, hass_ws_client, setup_api, mock_cloud_login): - """Test we cannot enable remote UI when trusted networks active.""" + """Test we can enable remote UI when trusted networks active.""" hass.auth._providers[('trusted_networks', None)] = \ tn_auth.TrustedNetworksAuthProvider( hass, None, tn_auth.CONFIG_SCHEMA({ @@ -749,3 +750,53 @@ async def test_update_google_entity( 'aliases': ['lefty', 'righty'], 'disable_2fa': False, } + + +async def test_enabling_remote_trusted_proxies_local4( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.http.trusted_proxies.append(ip_network('127.0.0.1')) + + client = await hass_ws_client(hass) + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + side_effect=AssertionError + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 500 + assert response['error']['message'] == \ + 'Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.' + + assert len(mock_connect.mock_calls) == 0 + + +async def test_enabling_remote_trusted_proxies_local6( + hass, hass_ws_client, setup_api, mock_cloud_login): + """Test we cannot enable remote UI when trusted networks active.""" + hass.http.trusted_proxies.append(ip_network('::1')) + + client = await hass_ws_client(hass) + + with patch( + 'hass_nabucasa.remote.RemoteUI.connect', + side_effect=AssertionError + ) as mock_connect: + await client.send_json({ + 'id': 5, + 'type': 'cloud/remote/connect', + }) + response = await client.receive_json() + + assert not response['success'] + assert response['error']['code'] == 500 + assert response['error']['message'] == \ + 'Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.' + + assert len(mock_connect.mock_calls) == 0 From dc93779f02eb450d7eba4f08302ca7a87ae873bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Jun 2019 23:13:57 -0700 Subject: [PATCH 08/10] Bumped version to 0.94.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 58897e78d0c..52d13ec58a6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 94 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From bfafe9ccbe03862de7a5f583ea8b312102d0c39c Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Sat, 8 Jun 2019 14:21:41 +0800 Subject: [PATCH 09/10] Fix for sun issues (#24309) --- homeassistant/components/sun/__init__.py | 21 ++++++++++++++------- tests/components/sun/test_init.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index dda692a8d80..edb2549164b 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -3,7 +3,8 @@ import logging from datetime import timedelta from homeassistant.const import ( - CONF_ELEVATION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + CONF_ELEVATION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, + EVENT_CORE_CONFIG_UPDATE) from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time @@ -70,7 +71,7 @@ async def async_setup(hass, config): _LOGGER.warning( "Elevation is now configured in home assistant core. " "See https://home-assistant.io/docs/configuration/basic/") - Sun(hass, get_astral_location(hass)) + Sun(hass) return True @@ -79,18 +80,23 @@ class Sun(Entity): entity_id = ENTITY_ID - def __init__(self, hass, location): + def __init__(self, hass): """Initialize the sun.""" self.hass = hass - self.location = location + self.location = None self._state = self.next_rising = self.next_setting = None self.next_dawn = self.next_dusk = None self.next_midnight = self.next_noon = None self.solar_elevation = self.solar_azimuth = None self.rising = self.phase = None - self._next_change = None - self.update_events(dt_util.utcnow()) + + def update_location(event): + self.location = get_astral_location(self.hass) + self.update_events(dt_util.utcnow()) + update_location(None) + self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, update_location) @property def name(self): @@ -100,7 +106,8 @@ class Sun(Entity): @property def state(self): """Return the state of the sun.""" - if self.next_rising > self.next_setting: + # 0.8333 is the same value as astral uses + if self.solar_elevation > -0.833: return STATE_ABOVE_HORIZON return STATE_BELOW_HORIZON diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 374527e2c8a..26d6bd73fed 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -119,6 +119,14 @@ async def test_state_change(hass): assert sun.STATE_ABOVE_HORIZON == \ hass.states.get(sun.ENTITY_ID).state + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=now): + await hass.config.async_update(longitude=hass.config.longitude+90) + await hass.async_block_till_done() + + assert sun.STATE_ABOVE_HORIZON == \ + hass.states.get(sun.ENTITY_ID).state + async def test_norway_in_june(hass): """Test location in Norway where the sun doesn't set in summer.""" @@ -142,6 +150,8 @@ async def test_norway_in_june(hass): state.attributes[sun.STATE_ATTR_NEXT_SETTING]) == \ datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) + assert state.state == sun.STATE_ABOVE_HORIZON + @mark.skip async def test_state_change_count(hass): From b68a796c7ce5d33bbf70fb21b7cd0ee91509d451 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 8 Jun 2019 08:43:18 +0200 Subject: [PATCH 10/10] deCONZ - properly identify configured bridge (#24378) --- .../components/deconz/config_flow.py | 22 +++++++++++-------- tests/components/deconz/test_config_flow.py | 13 ++++++++--- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index cf172ad7991..ea93cc590e2 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -18,6 +18,7 @@ from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de' CONF_SERIAL = 'serial' +ATTR_UUID = 'udn' @callback @@ -156,25 +157,28 @@ class DeconzFlowHandler(config_entries.ConfigFlow): if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason='not_deconz_bridge') - bridgeid = discovery_info[ATTR_SERIAL] - gateway_entries = configured_gateways(self.hass) + uuid = discovery_info[ATTR_UUID].replace('uuid:', '') + gateways = { + gateway.api.config.uuid: gateway + for gateway in self.hass.data.get(DOMAIN, {}).values() + } - if bridgeid in gateway_entries: - entry = gateway_entries[bridgeid] + if uuid in gateways: + entry = gateways[uuid].config_entry await self._update_entry(entry, discovery_info[CONF_HOST]) return self.async_abort(reason='updated_instance') - # pylint: disable=unsupported-assignment-operation - self.context[ATTR_SERIAL] = bridgeid - - if any(bridgeid == flow['context'][ATTR_SERIAL] + bridgeid = discovery_info[ATTR_SERIAL] + if any(bridgeid == flow['context'][CONF_BRIDGEID] for flow in self._async_in_progress()): return self.async_abort(reason='already_in_progress') + # pylint: disable=unsupported-assignment-operation + self.context[CONF_BRIDGEID] = bridgeid + deconz_config = { CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: discovery_info[CONF_PORT], - CONF_BRIDGEID: bridgeid } return await self.async_step_import(deconz_config) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 2b9f2c013b0..ac22c964151 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for deCONZ config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch import asyncio @@ -177,7 +177,8 @@ async def test_bridge_ssdp_discovery(hass): config_flow.CONF_PORT: 80, config_flow.ATTR_SERIAL: 'id', config_flow.ATTR_MANUFACTURERURL: - config_flow.DECONZ_MANUFACTURERURL + config_flow.DECONZ_MANUFACTURERURL, + config_flow.ATTR_UUID: 'uuid:1234' }, context={'source': 'ssdp'} ) @@ -207,13 +208,19 @@ async def test_bridge_discovery_update_existing_entry(hass): }) entry.add_to_hass(hass) + gateway = Mock() + gateway.config_entry = entry + gateway.api.config.uuid = '1234' + hass.data[config_flow.DOMAIN] = {'id': gateway} + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ config_flow.CONF_HOST: 'mock-deconz', config_flow.ATTR_SERIAL: 'id', config_flow.ATTR_MANUFACTURERURL: - config_flow.DECONZ_MANUFACTURERURL + config_flow.DECONZ_MANUFACTURERURL, + config_flow.ATTR_UUID: 'uuid:1234' }, context={'source': 'ssdp'} )