Merge pull request #24396 from home-assistant/rc

0.94.1
This commit is contained in:
Paulus Schoutsen 2019-06-07 23:45:13 -07:00 committed by GitHub
commit 282b4f4927
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 388 additions and 109 deletions

View File

@ -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):

View File

@ -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"]

View File

@ -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."""

View File

@ -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.'),
} }

View File

@ -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

View File

@ -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)

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -11,6 +11,11 @@
"Royal Philips Electronics" "Royal Philips Electronics"
] ]
}, },
"homekit": {
"models": [
"BSB002"
]
},
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [
"@balloob" "@balloob"

View File

@ -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

View File

@ -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:

View File

@ -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():

View File

@ -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": [

View File

@ -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

View 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)

View File

@ -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"

View 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."
}
}
}

View File

@ -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."""

View File

@ -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"],

View File

@ -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(

View File

@ -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)

View File

@ -49,6 +49,7 @@ FLOWS = [
"twilio", "twilio",
"unifi", "unifi",
"upnp", "upnp",
"wemo",
"zha", "zha",
"zone", "zone",
"zwave" "zwave"

View File

@ -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"

View File

@ -20,5 +20,8 @@ ZEROCONF = {
} }
HOMEKIT = { HOMEKIT = {
"LIFX ": "lifx" "BSB002": "hue",
"LIFX": "lifx",
"TRADFRI": "tradfri",
"Wemo": "wemo"
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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'}
) )

View File

@ -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,

View File

@ -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'

View File

@ -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):

View File

@ -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'