mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
commit
282b4f4927
@ -190,6 +190,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
self._last_triggered = None
|
self._last_triggered = None
|
||||||
self._hidden = hidden
|
self._hidden = hidden
|
||||||
self._initial_state = initial_state
|
self._initial_state = initial_state
|
||||||
|
self._is_enabled = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -216,7 +217,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if entity is on."""
|
"""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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Startup with initial state or previous state."""
|
"""Startup with initial state or previous state."""
|
||||||
@ -239,37 +241,16 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
"initial state", self.entity_id,
|
"initial state", self.entity_id,
|
||||||
enable_automation)
|
enable_automation)
|
||||||
|
|
||||||
if not enable_automation:
|
if 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:
|
|
||||||
await self.async_enable()
|
await self.async_enable()
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs) -> None:
|
async def async_turn_on(self, **kwargs) -> None:
|
||||||
"""Turn the entity on and update the state."""
|
"""Turn the entity on and update the state."""
|
||||||
if self.is_on:
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.async_enable()
|
await self.async_enable()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs) -> None:
|
async def async_turn_off(self, **kwargs) -> None:
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
if not self.is_on:
|
await self.async_disable()
|
||||||
return
|
|
||||||
|
|
||||||
self._async_detach_triggers()
|
|
||||||
self._async_detach_triggers = None
|
|
||||||
await self.async_update_ha_state()
|
|
||||||
|
|
||||||
async def async_trigger(self, variables, skip_condition=False,
|
async def async_trigger(self, variables, skip_condition=False,
|
||||||
context=None):
|
context=None):
|
||||||
@ -296,19 +277,51 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
|||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
"""Remove listeners when removing automation from HASS."""
|
"""Remove listeners when removing automation from HASS."""
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
await self.async_turn_off()
|
await self.async_disable()
|
||||||
|
|
||||||
async def async_enable(self):
|
async def async_enable(self):
|
||||||
"""Enable this automation entity.
|
"""Enable this automation entity.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
if self.is_on:
|
if self._is_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
return
|
||||||
|
|
||||||
self._async_detach_triggers = await self._async_attach_triggers(
|
self._async_detach_triggers = await self._async_attach_triggers(
|
||||||
self.async_trigger)
|
self.async_trigger)
|
||||||
await self.async_update_ha_state()
|
|
||||||
|
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
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Axis",
|
"name": "Axis",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/components/axis",
|
"documentation": "https://www.home-assistant.io/components/axis",
|
||||||
"requirements": ["axis==24"],
|
"requirements": ["axis==25"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"zeroconf": ["_axis-video._tcp.local."],
|
"zeroconf": ["_axis-video._tcp.local."],
|
||||||
"codeowners": ["@kane610"]
|
"codeowners": ["@kane610"]
|
||||||
|
@ -38,3 +38,7 @@ DISPATCHER_REMOTE_UPDATE = 'cloud_remote_update'
|
|||||||
|
|
||||||
class InvalidTrustedNetworks(Exception):
|
class InvalidTrustedNetworks(Exception):
|
||||||
"""Raised when invalid trusted networks config."""
|
"""Raised when invalid trusted networks config."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidTrustedProxies(Exception):
|
||||||
|
"""Raised when invalid trusted proxies config."""
|
||||||
|
@ -18,7 +18,8 @@ from homeassistant.components.google_assistant import helpers as google_helpers
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -52,7 +53,10 @@ SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|||||||
_CLOUD_ERRORS = {
|
_CLOUD_ERRORS = {
|
||||||
InvalidTrustedNetworks:
|
InvalidTrustedNetworks:
|
||||||
(500, 'Remote UI not compatible with 127.0.0.1/::1'
|
(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.'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from .const import (
|
|||||||
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
|
PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
|
||||||
PREF_ALIASES, PREF_SHOULD_EXPOSE,
|
PREF_ALIASES, PREF_SHOULD_EXPOSE,
|
||||||
InvalidTrustedNetworks)
|
InvalidTrustedNetworks, InvalidTrustedProxies)
|
||||||
|
|
||||||
STORAGE_KEY = DOMAIN
|
STORAGE_KEY = DOMAIN
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
@ -59,6 +59,9 @@ class CloudPreferences:
|
|||||||
if remote_enabled is True and self._has_local_trusted_network:
|
if remote_enabled is True and self._has_local_trusted_network:
|
||||||
raise InvalidTrustedNetworks
|
raise InvalidTrustedNetworks
|
||||||
|
|
||||||
|
if remote_enabled is True and self._has_local_trusted_proxies:
|
||||||
|
raise InvalidTrustedProxies
|
||||||
|
|
||||||
await self._store.async_save(self._prefs)
|
await self._store.async_save(self._prefs)
|
||||||
|
|
||||||
async def async_update_google_entity_config(
|
async def async_update_google_entity_config(
|
||||||
@ -112,7 +115,7 @@ class CloudPreferences:
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self._has_local_trusted_network:
|
if self._has_local_trusted_network or self._has_local_trusted_proxies:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -162,3 +165,18 @@ class CloudPreferences:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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
|
||||||
|
@ -18,6 +18,7 @@ from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
|
|||||||
|
|
||||||
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
|
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
|
||||||
CONF_SERIAL = 'serial'
|
CONF_SERIAL = 'serial'
|
||||||
|
ATTR_UUID = 'udn'
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -156,25 +157,28 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
|
|||||||
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
|
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
|
||||||
return self.async_abort(reason='not_deconz_bridge')
|
return self.async_abort(reason='not_deconz_bridge')
|
||||||
|
|
||||||
bridgeid = discovery_info[ATTR_SERIAL]
|
uuid = discovery_info[ATTR_UUID].replace('uuid:', '')
|
||||||
gateway_entries = configured_gateways(self.hass)
|
gateways = {
|
||||||
|
gateway.api.config.uuid: gateway
|
||||||
|
for gateway in self.hass.data.get(DOMAIN, {}).values()
|
||||||
|
}
|
||||||
|
|
||||||
if bridgeid in gateway_entries:
|
if uuid in gateways:
|
||||||
entry = gateway_entries[bridgeid]
|
entry = gateways[uuid].config_entry
|
||||||
await self._update_entry(entry, discovery_info[CONF_HOST])
|
await self._update_entry(entry, discovery_info[CONF_HOST])
|
||||||
return self.async_abort(reason='updated_instance')
|
return self.async_abort(reason='updated_instance')
|
||||||
|
|
||||||
# pylint: disable=unsupported-assignment-operation
|
bridgeid = discovery_info[ATTR_SERIAL]
|
||||||
self.context[ATTR_SERIAL] = bridgeid
|
if any(bridgeid == flow['context'][CONF_BRIDGEID]
|
||||||
|
|
||||||
if any(bridgeid == flow['context'][ATTR_SERIAL]
|
|
||||||
for flow in self._async_in_progress()):
|
for flow in self._async_in_progress()):
|
||||||
return self.async_abort(reason='already_in_progress')
|
return self.async_abort(reason='already_in_progress')
|
||||||
|
|
||||||
|
# pylint: disable=unsupported-assignment-operation
|
||||||
|
self.context[CONF_BRIDGEID] = bridgeid
|
||||||
|
|
||||||
deconz_config = {
|
deconz_config = {
|
||||||
CONF_HOST: discovery_info[CONF_HOST],
|
CONF_HOST: discovery_info[CONF_HOST],
|
||||||
CONF_PORT: discovery_info[CONF_PORT],
|
CONF_PORT: discovery_info[CONF_PORT],
|
||||||
CONF_BRIDGEID: bridgeid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await self.async_step_import(deconz_config)
|
return await self.async_step_import(deconz_config)
|
||||||
|
@ -58,7 +58,6 @@ SERVICE_HANDLERS = {
|
|||||||
SERVICE_MOBILE_APP: ('mobile_app', None),
|
SERVICE_MOBILE_APP: ('mobile_app', None),
|
||||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||||
SERVICE_NETGEAR: ('device_tracker', None),
|
SERVICE_NETGEAR: ('device_tracker', None),
|
||||||
SERVICE_WEMO: ('wemo', None),
|
|
||||||
SERVICE_HASSIO: ('hassio', None),
|
SERVICE_HASSIO: ('hassio', None),
|
||||||
SERVICE_APPLE_TV: ('apple_tv', None),
|
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||||
SERVICE_ENIGMA2: ('media_player', 'enigma2'),
|
SERVICE_ENIGMA2: ('media_player', 'enigma2'),
|
||||||
@ -94,19 +93,20 @@ OPTIONAL_SERVICE_HANDLERS = {
|
|||||||
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
|
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
|
||||||
}
|
}
|
||||||
|
|
||||||
MIGRATED_SERVICE_HANDLERS = {
|
MIGRATED_SERVICE_HANDLERS = [
|
||||||
'axis': None,
|
'axis',
|
||||||
'deconz': None,
|
'deconz',
|
||||||
'esphome': None,
|
'esphome',
|
||||||
'ikea_tradfri': None,
|
'ikea_tradfri',
|
||||||
'homekit': None,
|
'homekit',
|
||||||
'philips_hue': None
|
'philips_hue',
|
||||||
}
|
SERVICE_WEMO,
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \
|
DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \
|
||||||
list(MIGRATED_SERVICE_HANDLERS)
|
MIGRATED_SERVICE_HANDLERS
|
||||||
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \
|
DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \
|
||||||
list(MIGRATED_SERVICE_HANDLERS)
|
MIGRATED_SERVICE_HANDLERS
|
||||||
|
|
||||||
CONF_IGNORE = 'ignore'
|
CONF_IGNORE = 'ignore'
|
||||||
CONF_ENABLE = 'enable'
|
CONF_ENABLE = 'enable'
|
||||||
|
@ -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'
|
||||||
|
@ -228,6 +228,7 @@ class HomeAssistantHTTP:
|
|||||||
self.ssl_key = ssl_key
|
self.ssl_key = ssl_key
|
||||||
self.server_host = server_host
|
self.server_host = server_host
|
||||||
self.server_port = server_port
|
self.server_port = server_port
|
||||||
|
self.trusted_proxies = trusted_proxies
|
||||||
self.is_ban_enabled = is_ban_enabled
|
self.is_ban_enabled = is_ban_enabled
|
||||||
self.ssl_profile = ssl_profile
|
self.ssl_profile = ssl_profile
|
||||||
self._handler = None
|
self._handler = None
|
||||||
|
@ -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"
|
||||||
|
@ -3,7 +3,8 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from homeassistant.const import (
|
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.core import callback
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
@ -70,7 +71,7 @@ async def async_setup(hass, config):
|
|||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Elevation is now configured in home assistant core. "
|
"Elevation is now configured in home assistant core. "
|
||||||
"See https://home-assistant.io/docs/configuration/basic/")
|
"See https://home-assistant.io/docs/configuration/basic/")
|
||||||
Sun(hass, get_astral_location(hass))
|
Sun(hass)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -79,18 +80,23 @@ class Sun(Entity):
|
|||||||
|
|
||||||
entity_id = ENTITY_ID
|
entity_id = ENTITY_ID
|
||||||
|
|
||||||
def __init__(self, hass, location):
|
def __init__(self, hass):
|
||||||
"""Initialize the sun."""
|
"""Initialize the sun."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.location = location
|
self.location = None
|
||||||
self._state = self.next_rising = self.next_setting = None
|
self._state = self.next_rising = self.next_setting = None
|
||||||
self.next_dawn = self.next_dusk = None
|
self.next_dawn = self.next_dusk = None
|
||||||
self.next_midnight = self.next_noon = None
|
self.next_midnight = self.next_noon = None
|
||||||
self.solar_elevation = self.solar_azimuth = None
|
self.solar_elevation = self.solar_azimuth = None
|
||||||
self.rising = self.phase = None
|
self.rising = self.phase = None
|
||||||
|
|
||||||
self._next_change = None
|
self._next_change = None
|
||||||
|
|
||||||
|
def update_location(event):
|
||||||
|
self.location = get_astral_location(self.hass)
|
||||||
self.update_events(dt_util.utcnow())
|
self.update_events(dt_util.utcnow())
|
||||||
|
update_location(None)
|
||||||
|
self.hass.bus.async_listen(
|
||||||
|
EVENT_CORE_CONFIG_UPDATE, update_location)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -100,7 +106,8 @@ class Sun(Entity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sun."""
|
"""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_ABOVE_HORIZON
|
||||||
|
|
||||||
return STATE_BELOW_HORIZON
|
return STATE_BELOW_HORIZON
|
||||||
|
@ -42,6 +42,7 @@ class TadoDeviceScanner(DeviceScanner):
|
|||||||
|
|
||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
|
self.hass = hass
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
|
|
||||||
self.username = config[CONF_USERNAME]
|
self.username = config[CONF_USERNAME]
|
||||||
@ -60,8 +61,7 @@ class TadoDeviceScanner(DeviceScanner):
|
|||||||
# The API URL always needs a username and password
|
# The API URL always needs a username and password
|
||||||
self.tadoapiurl += '?username={username}&password={password}'
|
self.tadoapiurl += '?username={username}&password={password}'
|
||||||
|
|
||||||
self.websession = async_create_clientsession(
|
self.websession = None
|
||||||
hass, cookie_jar=aiohttp.CookieJar(unsafe=True))
|
|
||||||
|
|
||||||
self.success_init = asyncio.run_coroutine_threadsafe(
|
self.success_init = asyncio.run_coroutine_threadsafe(
|
||||||
self._async_update_info(), hass.loop
|
self._async_update_info(), hass.loop
|
||||||
@ -92,6 +92,10 @@ class TadoDeviceScanner(DeviceScanner):
|
|||||||
"""
|
"""
|
||||||
_LOGGER.debug("Requesting Tado")
|
_LOGGER.debug("Requesting Tado")
|
||||||
|
|
||||||
|
if self.websession is None:
|
||||||
|
self.websession = async_create_clientsession(
|
||||||
|
self.hass, cookie_jar=aiohttp.CookieJar(unsafe=True))
|
||||||
|
|
||||||
last_results = []
|
last_results = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -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": [
|
||||||
|
@ -4,6 +4,7 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.discovery import SERVICE_WEMO
|
from homeassistant.components.discovery import SERVICE_WEMO
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
@ -68,22 +69,35 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up for WeMo devices."""
|
"""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
|
import pywemo
|
||||||
|
|
||||||
|
config = hass.data[DOMAIN]
|
||||||
|
|
||||||
# Keep track of WeMo devices
|
# Keep track of WeMo devices
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
# Keep track of WeMo device subscriptions for push updates
|
# Keep track of WeMo device subscriptions for push updates
|
||||||
global SUBSCRIPTION_REGISTRY
|
global SUBSCRIPTION_REGISTRY
|
||||||
SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry()
|
SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry()
|
||||||
SUBSCRIPTION_REGISTRY.start()
|
await hass.async_add_executor_job(SUBSCRIPTION_REGISTRY.start)
|
||||||
|
|
||||||
def stop_wemo(event):
|
def stop_wemo(event):
|
||||||
"""Shutdown Wemo subscriptions and subscription thread on exit."""
|
"""Shutdown Wemo subscriptions and subscription thread on exit."""
|
||||||
_LOGGER.debug("Shutting down WeMo event subscriptions")
|
_LOGGER.debug("Shutting down WeMo event subscriptions")
|
||||||
SUBSCRIPTION_REGISTRY.stop()
|
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):
|
def setup_url_for_device(device):
|
||||||
"""Determine setup.xml url for given device."""
|
"""Determine setup.xml url for given device."""
|
||||||
@ -119,7 +133,7 @@ def setup(hass, config):
|
|||||||
discovery.load_platform(
|
discovery.load_platform(
|
||||||
hass, component, DOMAIN, discovery_info, config)
|
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):
|
def discover_wemo_devices(now):
|
||||||
"""Run discovery for WeMo devices."""
|
"""Run discovery for WeMo devices."""
|
||||||
@ -145,7 +159,7 @@ def setup(hass, config):
|
|||||||
if d[1].serialnumber == device.serialnumber]:
|
if d[1].serialnumber == device.serialnumber]:
|
||||||
devices.append((url, device))
|
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...")
|
_LOGGER.debug("Scanning network for WeMo devices...")
|
||||||
for device in pywemo.discover_devices():
|
for device in pywemo.discover_devices():
|
||||||
if not [d[1] for d in devices
|
if not [d[1] for d in devices
|
||||||
@ -168,6 +182,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
_LOGGER.debug("WeMo device discovery has finished")
|
_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
|
return True
|
||||||
|
15
homeassistant/components/wemo/config_flow.py
Normal file
15
homeassistant/components/wemo/config_flow.py
Normal file
@ -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)
|
@ -1,10 +1,21 @@
|
|||||||
{
|
{
|
||||||
"domain": "wemo",
|
"domain": "wemo",
|
||||||
"name": "Wemo",
|
"name": "Wemo",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/components/wemo",
|
"documentation": "https://www.home-assistant.io/components/wemo",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pywemo==0.4.34"
|
"pywemo==0.4.34"
|
||||||
],
|
],
|
||||||
|
"ssdp": {
|
||||||
|
"manufacturer": [
|
||||||
|
"Belkin International Inc."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"homekit": {
|
||||||
|
"models": [
|
||||||
|
"Wemo"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@sqldiablo"
|
"@sqldiablo"
|
||||||
|
15
homeassistant/components/wemo/strings.json
Normal file
15
homeassistant/components/wemo/strings.json
Normal file
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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."""
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Wink",
|
"name": "Wink",
|
||||||
"documentation": "https://www.home-assistant.io/components/wink",
|
"documentation": "https://www.home-assistant.io/components/wink",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pubnubsub-handler==1.0.6",
|
"pubnubsub-handler==1.0.7",
|
||||||
"python-wink==1.10.5"
|
"python-wink==1.10.5"
|
||||||
],
|
],
|
||||||
"dependencies": ["configurator"],
|
"dependencies": ["configurator"],
|
||||||
|
@ -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(
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 94
|
MINOR_VERSION = 94
|
||||||
PATCH_VERSION = '0'
|
PATCH_VERSION = '1'
|
||||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||||
|
@ -49,6 +49,7 @@ FLOWS = [
|
|||||||
"twilio",
|
"twilio",
|
||||||
"unifi",
|
"unifi",
|
||||||
"upnp",
|
"upnp",
|
||||||
|
"wemo",
|
||||||
"zha",
|
"zha",
|
||||||
"zone",
|
"zone",
|
||||||
"zwave"
|
"zwave"
|
||||||
|
@ -7,6 +7,9 @@ To update, run python3 -m hassfest
|
|||||||
SSDP = {
|
SSDP = {
|
||||||
"device_type": {},
|
"device_type": {},
|
||||||
"manufacturer": {
|
"manufacturer": {
|
||||||
|
"Belkin International Inc.": [
|
||||||
|
"wemo"
|
||||||
|
],
|
||||||
"Royal Philips Electronics": [
|
"Royal Philips Electronics": [
|
||||||
"deconz",
|
"deconz",
|
||||||
"hue"
|
"hue"
|
||||||
|
@ -20,5 +20,8 @@ ZEROCONF = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HOMEKIT = {
|
HOMEKIT = {
|
||||||
"LIFX ": "lifx"
|
"BSB002": "hue",
|
||||||
|
"LIFX": "lifx",
|
||||||
|
"TRADFRI": "tradfri",
|
||||||
|
"Wemo": "wemo"
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ av==6.1.2
|
|||||||
# avion==0.10
|
# avion==0.10
|
||||||
|
|
||||||
# homeassistant.components.axis
|
# homeassistant.components.axis
|
||||||
axis==24
|
axis==25
|
||||||
|
|
||||||
# homeassistant.components.azure_event_hub
|
# homeassistant.components.azure_event_hub
|
||||||
azure-eventhub==1.3.1
|
azure-eventhub==1.3.1
|
||||||
@ -918,7 +918,7 @@ psutil==5.6.2
|
|||||||
ptvsd==4.2.8
|
ptvsd==4.2.8
|
||||||
|
|
||||||
# homeassistant.components.wink
|
# homeassistant.components.wink
|
||||||
pubnubsub-handler==1.0.6
|
pubnubsub-handler==1.0.7
|
||||||
|
|
||||||
# homeassistant.components.pushbullet
|
# homeassistant.components.pushbullet
|
||||||
pushbullet.py==0.11.0
|
pushbullet.py==0.11.0
|
||||||
|
@ -70,7 +70,7 @@ apns2==0.3.0
|
|||||||
av==6.1.2
|
av==6.1.2
|
||||||
|
|
||||||
# homeassistant.components.axis
|
# homeassistant.components.axis
|
||||||
axis==24
|
axis==25
|
||||||
|
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
bellows-homeassistant==0.7.3
|
bellows-homeassistant==0.7.3
|
||||||
|
@ -43,7 +43,9 @@ 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:
|
||||||
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(
|
integration.add_error(
|
||||||
'ssdp', 'Config flow has no async_step_ssdp')
|
'ssdp', 'Config flow has no async_step_ssdp')
|
||||||
continue
|
continue
|
||||||
|
@ -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',
|
||||||
|
@ -29,7 +29,7 @@ def test_if_fires_on_hass_start(hass):
|
|||||||
|
|
||||||
res = yield from async_setup_component(hass, automation.DOMAIN, config)
|
res = yield from async_setup_component(hass, automation.DOMAIN, config)
|
||||||
assert res
|
assert res
|
||||||
assert not automation.is_on(hass, 'automation.hello')
|
assert automation.is_on(hass, 'automation.hello')
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
yield from hass.async_start()
|
yield from hass.async_start()
|
||||||
@ -64,7 +64,7 @@ def test_if_fires_on_hass_shutdown(hass):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
assert res
|
assert res
|
||||||
assert not automation.is_on(hass, 'automation.hello')
|
assert automation.is_on(hass, 'automation.hello')
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
yield from hass.async_start()
|
yield from hass.async_start()
|
||||||
|
@ -696,12 +696,12 @@ def test_initial_value_off(hass):
|
|||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_initial_value_on(hass):
|
||||||
def test_initial_value_on(hass):
|
|
||||||
"""Test initial value on."""
|
"""Test initial value on."""
|
||||||
|
hass.state = CoreState.not_running
|
||||||
calls = async_mock_service(hass, 'test', 'automation')
|
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: {
|
automation.DOMAIN: {
|
||||||
'alias': 'hello',
|
'alias': 'hello',
|
||||||
'initial_state': 'on',
|
'initial_state': 'on',
|
||||||
@ -715,23 +715,23 @@ def test_initial_value_on(hass):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
assert res
|
|
||||||
assert automation.is_on(hass, 'automation.hello')
|
assert automation.is_on(hass, 'automation.hello')
|
||||||
|
|
||||||
|
await hass.async_start()
|
||||||
hass.bus.async_fire('test_event')
|
hass.bus.async_fire('test_event')
|
||||||
yield from hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def test_initial_value_off_but_restore_on(hass):
|
||||||
def test_initial_value_off_but_restore_on(hass):
|
|
||||||
"""Test initial value off and restored state is turned on."""
|
"""Test initial value off and restored state is turned on."""
|
||||||
|
hass.state = CoreState.not_running
|
||||||
calls = async_mock_service(hass, 'test', 'automation')
|
calls = async_mock_service(hass, 'test', 'automation')
|
||||||
mock_restore_cache(hass, (
|
mock_restore_cache(hass, (
|
||||||
State('automation.hello', STATE_ON),
|
State('automation.hello', STATE_ON),
|
||||||
))
|
))
|
||||||
|
|
||||||
res = yield from async_setup_component(hass, automation.DOMAIN, {
|
await async_setup_component(hass, automation.DOMAIN, {
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
'alias': 'hello',
|
'alias': 'hello',
|
||||||
'initial_state': 'off',
|
'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')
|
assert not automation.is_on(hass, 'automation.hello')
|
||||||
|
|
||||||
|
await hass.async_start()
|
||||||
hass.bus.async_fire('test_event')
|
hass.bus.async_fire('test_event')
|
||||||
yield from hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
|
||||||
@ -858,7 +858,7 @@ def test_automation_not_trigger_on_bootstrap(hass):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
assert res
|
assert res
|
||||||
assert not automation.is_on(hass, 'automation.hello')
|
assert automation.is_on(hass, 'automation.hello')
|
||||||
|
|
||||||
hass.bus.async_fire('test_event')
|
hass.bus.async_fire('test_event')
|
||||||
yield from hass.async_block_till_done()
|
yield from hass.async_block_till_done()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Tests for the HTTP API for the cloud component."""
|
"""Tests for the HTTP API for the cloud component."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
from ipaddress import ip_network
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
@ -672,7 +673,7 @@ async def test_enabling_remote_trusted_networks_local6(
|
|||||||
|
|
||||||
async def test_enabling_remote_trusted_networks_other(
|
async def test_enabling_remote_trusted_networks_other(
|
||||||
hass, hass_ws_client, setup_api, mock_cloud_login):
|
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)] = \
|
hass.auth._providers[('trusted_networks', None)] = \
|
||||||
tn_auth.TrustedNetworksAuthProvider(
|
tn_auth.TrustedNetworksAuthProvider(
|
||||||
hass, None, tn_auth.CONFIG_SCHEMA({
|
hass, None, tn_auth.CONFIG_SCHEMA({
|
||||||
@ -749,3 +750,53 @@ async def test_update_google_entity(
|
|||||||
'aliases': ['lefty', 'righty'],
|
'aliases': ['lefty', 'righty'],
|
||||||
'disable_2fa': False,
|
'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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Tests for deCONZ config flow."""
|
"""Tests for deCONZ config flow."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
@ -177,7 +177,8 @@ async def test_bridge_ssdp_discovery(hass):
|
|||||||
config_flow.CONF_PORT: 80,
|
config_flow.CONF_PORT: 80,
|
||||||
config_flow.ATTR_SERIAL: 'id',
|
config_flow.ATTR_SERIAL: 'id',
|
||||||
config_flow.ATTR_MANUFACTURERURL:
|
config_flow.ATTR_MANUFACTURERURL:
|
||||||
config_flow.DECONZ_MANUFACTURERURL
|
config_flow.DECONZ_MANUFACTURERURL,
|
||||||
|
config_flow.ATTR_UUID: 'uuid:1234'
|
||||||
},
|
},
|
||||||
context={'source': 'ssdp'}
|
context={'source': 'ssdp'}
|
||||||
)
|
)
|
||||||
@ -207,13 +208,19 @@ async def test_bridge_discovery_update_existing_entry(hass):
|
|||||||
})
|
})
|
||||||
entry.add_to_hass(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(
|
result = await hass.config_entries.flow.async_init(
|
||||||
config_flow.DOMAIN,
|
config_flow.DOMAIN,
|
||||||
data={
|
data={
|
||||||
config_flow.CONF_HOST: 'mock-deconz',
|
config_flow.CONF_HOST: 'mock-deconz',
|
||||||
config_flow.ATTR_SERIAL: 'id',
|
config_flow.ATTR_SERIAL: 'id',
|
||||||
config_flow.ATTR_MANUFACTURERURL:
|
config_flow.ATTR_MANUFACTURERURL:
|
||||||
config_flow.DECONZ_MANUFACTURERURL
|
config_flow.DECONZ_MANUFACTURERURL,
|
||||||
|
config_flow.ATTR_UUID: 'uuid:1234'
|
||||||
},
|
},
|
||||||
context={'source': 'ssdp'}
|
context={'source': 'ssdp'}
|
||||||
)
|
)
|
||||||
|
@ -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'
|
||||||
|
@ -119,6 +119,14 @@ async def test_state_change(hass):
|
|||||||
assert sun.STATE_ABOVE_HORIZON == \
|
assert sun.STATE_ABOVE_HORIZON == \
|
||||||
hass.states.get(sun.ENTITY_ID).state
|
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):
|
async def test_norway_in_june(hass):
|
||||||
"""Test location in Norway where the sun doesn't set in summer."""
|
"""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]) == \
|
state.attributes[sun.STATE_ATTR_NEXT_SETTING]) == \
|
||||||
datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC)
|
datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC)
|
||||||
|
|
||||||
|
assert state.state == sun.STATE_ABOVE_HORIZON
|
||||||
|
|
||||||
|
|
||||||
@mark.skip
|
@mark.skip
|
||||||
async def test_state_change_count(hass):
|
async def test_state_change_count(hass):
|
||||||
|
@ -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."""
|
||||||
|
def mock_homekit_info(service_type, name):
|
||||||
return ServiceInfo(
|
return ServiceInfo(
|
||||||
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
|
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
|
||||||
priority=0, server='name.local.',
|
priority=0, server='name.local.',
|
||||||
properties={b'md': b'LIFX Bulb'})
|
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