Compare commits

..

43 Commits

Author SHA1 Message Date
Paulus Schoutsen c2218e8a64 Merge pull request #24465 from home-assistant/rc
0.94.2
2019-06-11 08:54:29 -07:00
Paulus Schoutsen 6ea0575a4a Update home zone when core config updated (#24237)
* Update home zone when core config updated

* Lint
2019-06-11 08:30:38 -07:00
Paulus Schoutsen d88d57f3bb Bumped version to 0.94.2 2019-06-10 16:06:12 -07:00
Paulus Schoutsen bd80346592 Sun listener to adapt to core config updates (#24464)
* Adaptable sun listener

* Lint
2019-06-10 16:06:07 -07:00
Paulus Schoutsen 7292f2be69 Update Hass.io when core config is updated (#24461)
* Update Hass.io when core config is updated

* Lint

* Fix tests

* Lint sigh
2019-06-10 16:06:06 -07:00
Andy Kittner 21d04b3e14 Remember gpslogger entities across restarts (fixes #24432) (#24444)
* Remember gpslogger entities across restarts (fixes #24432)

* oops, missed those changes

* Remove to do and set defaults to `None`
2019-06-10 16:06:06 -07:00
Robert Svensson 3c6235bee5 Axis discovery MAC filter (#24442)
* Make sure to abort if the MAC is not from Axis

* Fix tests

* Andrew Sayre suggestion

Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com>
2019-06-10 16:06:05 -07:00
Julien Brochet b0985bb459 Load the SSDP component only when it's needed (#24420)
* fix(hue): Load the SSDP component only when it's needed

* fix(deconz): Don't load the SSDP component when it's not needed

* Update config_flow.py

* Update test_config_flow.py
2019-06-10 16:06:04 -07:00
Robert Svensson 8e93d0a7a2 deCONZ fix retry set state(#24410) 2019-06-10 16:06:03 -07:00
Paulus Schoutsen 282b4f4927 Merge pull request #24396 from home-assistant/rc
0.94.1
2019-06-07 23:45:13 -07:00
Robert Svensson b68a796c7c deCONZ - properly identify configured bridge (#24378) 2019-06-07 23:43:59 -07:00
Penny Wood bfafe9ccbe Fix for sun issues (#24309) 2019-06-07 23:28:56 -07:00
Paulus Schoutsen dc93779f02 Bumped version to 0.94.1 2019-06-07 23:13:57 -07:00
Paulus Schoutsen 14066dfb5a Check cloud trusted proxies (#24395) 2019-06-07 23:13:45 -07:00
Paulus Schoutsen 7d9988fd75 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
2019-06-07 23:13:44 -07:00
Paulus Schoutsen 2fed016347 Fix automation failing to restore state (#24390)
* Fix automation off

* Fix tests
2019-06-07 23:13:43 -07:00
William Scanlon d1b82e9ede Updated pubnubsub-handler to 1.0.7 to fix crash on slow startup (#24388) 2019-06-07 23:13:43 -07:00
Robert Svensson b8e20fcadf Bump dependency (#24376) 2019-06-07 23:13:42 -07:00
Paulus Schoutsen ebc09017b8 Initiate websession inside event loop (#24331) 2019-06-07 23:13:42 -07:00
Paulus Schoutsen 798b72e164 Add a discovery config flow to Wemo (#24208) 2019-06-07 23:13:41 -07:00
Pascal Vizeli 09292d5918 Update azure-pipelines-release.yml for Azure Pipelines 2019-06-05 22:15:18 +02:00
Pascal Vizeli d78e132007 Merge pull request #24305 from home-assistant/rc
0.94.0
2019-06-05 18:35:04 +02:00
Robert Svensson 8d3c9bc2d0 Don't let zeroconf be smart with addresses (#24321) 2019-06-05 18:33:31 +02:00
Pascal Vizeli ce93a332a7 Update and rename azure-pipelines.yml to azure-pipelines-release.yml 2019-06-05 09:37:57 +02:00
Paulus Schoutsen 13c3833593 Bumped version to 0.94.0 2019-06-04 14:34:06 -07:00
Paulus Schoutsen d0715c75c0 Merge remote-tracking branch 'origin/master' into rc 2019-06-04 14:33:49 -07:00
Paulus Schoutsen eca424656a Fix OwnTracks race condition (#24303)
* Fix OwnTracks race condition

* Lint
2019-06-04 14:25:10 -07:00
Robert Svensson 3b60081e2a address is deprecated in favor of addresses (#24302) 2019-06-04 14:25:09 -07:00
Paulus Schoutsen 1096fe3d87 Bumped version to 0.94.0b8 2019-06-04 11:06:25 -07:00
Paulus Schoutsen 389da16947 Upgrade Zeroconf to 0.23 (#24300) 2019-06-04 11:06:19 -07:00
Paulus Schoutsen 185af1b42a Run SSDP discovery in parallel (#24299) 2019-06-04 11:06:18 -07:00
Pascal Vizeli d17f27b65c Create progress file for pip installs (#24297)
* Create progress file for pip installs

* fix dedlock

* unflacky test

* Address comments

* Lint

* Types
2019-06-04 11:06:18 -07:00
Paulus Schoutsen bb0867f1a8 Guard against bad states in Mobile App/OwnTracks (#24292) 2019-06-04 11:06:17 -07:00
Paulus Schoutsen b67d32824c Updated frontend to 20190604.0 2019-06-04 09:41:07 -07:00
Pascal Vizeli bad920fa87 Bumped version to 0.94.0b7 2019-06-04 12:42:45 +02:00
Paulus Schoutsen 281fe93a26 Bumped version to 0.94.0b6 2019-06-03 12:41:45 -07:00
Paulus Schoutsen 4a71593ffd Remove deps folder in config when on Docker (#24284)
* Remove deps folder in config

* Fix tests

* Fix tests with docker check
2019-06-03 12:41:36 -07:00
Paulus Schoutsen 014cc14b7e Fix cors on the index view (#24283) 2019-06-03 12:41:35 -07:00
Otto Winter ee71d2ca60 Bump aioesphomeapi to 2.1.0 (#24278)
* Bump aioesphomeapi to 2.1.0

* Update requirements txt
2019-06-03 12:41:34 -07:00
Paul Bottein 5085ce8ab1 Add temperature sensor support to google smarthome thermostat device (#24264)
* Add temperature sensor support to google smarthome thermostat device

* fix lint for trait_test

* Reset temperature unit in tests

* Address comment
2019-06-03 12:41:32 -07:00
Robert Svensson 9ed5b70d01 deCONZ migrate to SSDP discovery (#24252)
* Migrate deCONZ to use new SSDP discovery
Add new discovery info manufacturer URL to be able to separate Hue and deCONZ bridges

* Mark deCONZ as migrated in Discovery component

* Fix tests

* Fix Hue discovery ignore deCONZ bridge

* Less snake more badger

* Mushroom

* Fix indentation

* Config flow ignore manufacturer url that is not philips
2019-06-03 12:41:31 -07:00
Pascal Vizeli a00d8a493d Update azure-pipelines.yml for Azure Pipelines 2019-06-03 12:31:31 +02:00
Pascal Vizeli 2b0e56932b Update azure-pipelines.yml for Azure Pipelines 2019-06-03 11:51:34 +02:00
78 changed files with 1119 additions and 429 deletions
@@ -2,100 +2,20 @@
trigger:
batch: true
branches:
include:
- dev
tags:
include:
- '*'
pr: none
variables:
- name: versionBuilder
value: '3.2'
- name: versionWheels
value: '0.3'
- group: docker
- group: wheels
- group: github
- group: twine
jobs:
- job: 'Wheels'
condition: eq(variables['Build.SourceBranchName'], 'dev')
timeoutInMinutes: 360
pool:
vmImage: 'ubuntu-latest'
strategy:
maxParallel: 3
matrix:
amd64:
buildArch: 'amd64'
i386:
buildArch: 'i386'
armhf:
buildArch: 'armhf'
armv7:
buildArch: 'armv7'
aarch64:
buildArch: 'aarch64'
steps:
- script: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
sudo update-binfmts --enable qemu-arm
sudo update-binfmts --enable qemu-aarch64
displayName: 'Initial cross build'
- script: |
mkdir -p .ssh
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
chmod 600 .ssh/*
displayName: 'Install ssh key'
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
displayName: 'Install wheels builder'
- script: |
cp requirements_all.txt requirements_hassio.txt
# Enable because we can build it
sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
sed -i "s|# evdev|evdev|g" requirements_hassio.txt
sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
sed -i "s|# pycups|pycups|g" requirements_hassio.txt
sed -i "s|# homekit|homekit|g" requirements_hassio.txt
sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
sed -i "s|# decora|decora|g" requirements_hassio.txt
sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
# Disable because of error
sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
displayName: 'Prepare requirements files for Hass.io'
- script: |
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
homeassistant/$(buildArch)-wheels:$(versionWheels) \
--apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \
--index https://wheels.hass.io \
--requirement requirements_hassio.txt \
--upload rsync \
--remote wheels@$(wheelsHost):/opt/wheels
displayName: 'Run wheels build'
- job: 'VersionValidate'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
@@ -116,7 +36,6 @@ jobs:
fi
displayName: 'Check version of branch/tag'
- script: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
jq curl
@@ -159,7 +78,7 @@ jobs:
condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
dependsOn:
- 'VersionValidate'
timeoutInMinutes: 120
timeoutInMinutes: 240
pool:
vmImage: 'ubuntu-latest'
strategy:
@@ -214,7 +133,6 @@ jobs:
vmImage: 'ubuntu-latest'
steps:
- script: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
git jq curl
+42 -29
View File
@@ -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):
+7 -1
View File
@@ -14,6 +14,8 @@ from .const import CONF_MODEL, DOMAIN
from .device import get_device
from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect
AXIS_OUI = {'00408C', 'ACCC8E', 'B8A44F'}
CONFIG_FILE = 'axis.conf'
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
@@ -151,10 +153,14 @@ class AxisFlowHandler(config_entries.ConfigFlow):
This flow is triggered by the discovery component.
"""
serialnumber = discovery_info['properties']['macaddress']
if serialnumber[:6] not in AXIS_OUI:
return self.async_abort(reason='not_axis_device')
if discovery_info[CONF_HOST].startswith('169.254'):
return self.async_abort(reason='link_local_address')
serialnumber = discovery_info['properties']['macaddress']
# pylint: disable=unsupported-assignment-operation
self.context['macaddress'] = serialnumber
+1 -1
View File
@@ -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"]
+3 -2
View File
@@ -21,7 +21,8 @@
"abort": {
"already_configured": "Device is already configured",
"bad_config_file": "Bad data from config file",
"link_local_address": "Link local addresses are not supported"
"link_local_address": "Link local addresses are not supported",
"not_axis_device": "Discovered device not an Axis device"
}
}
}
}
+4
View File
@@ -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."""
+6 -2
View File
@@ -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.'),
}
+20 -2
View File
@@ -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
+1 -1
View File
@@ -56,7 +56,7 @@ async def websocket_update_config(hass, connection, msg):
data.pop('type')
try:
await hass.config.update(**data)
await hass.config.async_update(**data)
connection.send_result(msg['id'])
except ValueError as err:
connection.send_error(
+24 -9
View File
@@ -15,7 +15,9 @@ from homeassistant.helpers import aiohttp_client
from .const import CONF_BRIDGEID, DEFAULT_PORT, DOMAIN
DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
CONF_SERIAL = 'serial'
ATTR_UUID = 'udn'
@callback
@@ -149,23 +151,36 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
async def async_step_discovery(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge.
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered deCONZ bridge."""
from homeassistant.components.ssdp import (
ATTR_MANUFACTURERURL, ATTR_SERIAL)
This flow is triggered by the discovery component.
"""
bridgeid = discovery_info[CONF_SERIAL]
gateway_entries = configured_gateways(self.hass)
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
return self.async_abort(reason='not_deconz_bridge')
if bridgeid in gateway_entries:
entry = gateway_entries[bridgeid]
uuid = discovery_info[ATTR_UUID].replace('uuid:', '')
gateways = {
gateway.api.config.uuid: gateway
for gateway in self.hass.data.get(DOMAIN, {}).values()
}
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')
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: discovery_info[CONF_SERIAL]
}
return await self.async_step_import(deconz_config)
@@ -4,8 +4,13 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [
"pydeconz==59"
"pydeconz==60"
],
"ssdp": {
"manufacturer": [
"Royal Philips Electronics"
]
},
"dependencies": [],
"codeowners": [
"@kane610"
+4 -2
View File
@@ -34,9 +34,11 @@
},
"abort": {
"already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered",
"updated_instance": "Updated deCONZ instance with new host address",
"one_instance_only": "Component only supports one deCONZ instance"
"not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
}
}
}
+11 -12
View File
@@ -25,7 +25,6 @@ DOMAIN = 'discovery'
SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_APPLE_TV = 'apple_tv'
SERVICE_DAIKIN = 'daikin'
SERVICE_DECONZ = 'deconz'
SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ENIGMA2 = 'enigma2'
SERVICE_FREEBOX = 'freebox'
@@ -48,7 +47,6 @@ SERVICE_XIAOMI_GW = 'xiaomi_gw'
CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin',
SERVICE_DECONZ: 'deconz',
'google_cast': 'cast',
SERVICE_HEOS: 'heos',
SERVICE_TELLDUSLIVE: 'tellduslive',
@@ -60,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'),
@@ -96,18 +93,20 @@ OPTIONAL_SERVICE_HANDLERS = {
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
}
MIGRATED_SERVICE_HANDLERS = {
'axis': 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'
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/esphome",
"requirements": [
"aioesphomeapi==2.0.1"
"aioesphomeapi==2.1.0"
],
"dependencies": [],
"zeroconf": ["_esphomelib._tcp.local."],
@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/components/frontend",
"requirements": [
"home-assistant-frontend==20190602.0"
"home-assistant-frontend==20190604.0"
],
"dependencies": [
"api",
@@ -12,6 +12,7 @@ from homeassistant.components import (
media_player,
scene,
script,
sensor,
switch,
vacuum,
)
@@ -108,6 +109,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR,
(media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV,
(media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER,
(sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR,
}
CHALLENGE_ACK_NEEDED = 'ackNeeded'
@@ -13,6 +13,7 @@ from homeassistant.components import (
lock,
scene,
script,
sensor,
switch,
vacuum,
)
@@ -550,89 +551,126 @@ class TemperatureSettingTrait(_Trait):
@staticmethod
def supported(domain, features, device_class):
"""Test if state is supported."""
if domain != climate.DOMAIN:
return False
if domain == climate.DOMAIN:
return features & climate.SUPPORT_OPERATION_MODE
return features & climate.SUPPORT_OPERATION_MODE
return (domain == sensor.DOMAIN
and device_class == sensor.DEVICE_CLASS_TEMPERATURE)
def sync_attributes(self):
"""Return temperature point and modes attributes for a sync request."""
modes = []
supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
response = {}
attrs = self.state.attributes
domain = self.state.domain
response['thermostatTemperatureUnit'] = _google_temp_unit(
self.hass.config.units.temperature_unit)
if supported & climate.SUPPORT_ON_OFF != 0:
modes.append(STATE_OFF)
modes.append(STATE_ON)
if domain == sensor.DOMAIN:
device_class = attrs.get(ATTR_DEVICE_CLASS)
if device_class == sensor.DEVICE_CLASS_TEMPERATURE:
response["queryOnlyTemperatureSetting"] = True
if supported & climate.SUPPORT_OPERATION_MODE != 0:
for mode in self.state.attributes.get(climate.ATTR_OPERATION_LIST,
[]):
google_mode = self.hass_to_google.get(mode)
if google_mode and google_mode not in modes:
modes.append(google_mode)
elif domain == climate.DOMAIN:
modes = []
supported = attrs.get(ATTR_SUPPORTED_FEATURES)
return {
'availableThermostatModes': ','.join(modes),
'thermostatTemperatureUnit': _google_temp_unit(
self.hass.config.units.temperature_unit)
}
if supported & climate.SUPPORT_ON_OFF != 0:
modes.append(STATE_OFF)
modes.append(STATE_ON)
if supported & climate.SUPPORT_OPERATION_MODE != 0:
for mode in attrs.get(climate.ATTR_OPERATION_LIST, []):
google_mode = self.hass_to_google.get(mode)
if google_mode and google_mode not in modes:
modes.append(google_mode)
response['availableThermostatModes'] = ','.join(modes)
return response
def query_attributes(self):
"""Return temperature point and modes query attributes."""
attrs = self.state.attributes
response = {}
operation = attrs.get(climate.ATTR_OPERATION_MODE)
supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
if (supported & climate.SUPPORT_ON_OFF
and self.state.state == STATE_OFF):
response['thermostatMode'] = 'off'
elif (supported & climate.SUPPORT_OPERATION_MODE and
operation in self.hass_to_google):
response['thermostatMode'] = self.hass_to_google[operation]
elif supported & climate.SUPPORT_ON_OFF:
response['thermostatMode'] = 'on'
attrs = self.state.attributes
domain = self.state.domain
unit = self.hass.config.units.temperature_unit
if domain == sensor.DOMAIN:
device_class = attrs.get(ATTR_DEVICE_CLASS)
if device_class == sensor.DEVICE_CLASS_TEMPERATURE:
current_temp = self.state.state
if current_temp is not None:
response['thermostatTemperatureAmbient'] = \
round(temp_util.convert(
float(current_temp),
unit,
TEMP_CELSIUS
), 1)
current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
if current_temp is not None:
response['thermostatTemperatureAmbient'] = \
round(temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1)
elif domain == climate.DOMAIN:
operation = attrs.get(climate.ATTR_OPERATION_MODE)
supported = attrs.get(ATTR_SUPPORTED_FEATURES)
current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
if current_humidity is not None:
response['thermostatHumidityAmbient'] = current_humidity
if (supported & climate.SUPPORT_ON_OFF
and self.state.state == STATE_OFF):
response['thermostatMode'] = 'off'
elif (supported & climate.SUPPORT_OPERATION_MODE
and operation in self.hass_to_google):
response['thermostatMode'] = self.hass_to_google[operation]
elif supported & climate.SUPPORT_ON_OFF:
response['thermostatMode'] = 'on'
if operation == climate.STATE_AUTO:
if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
response['thermostatTemperatureSetpointHigh'] = \
current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
if current_temp is not None:
response['thermostatTemperatureAmbient'] = \
round(temp_util.convert(
attrs[climate.ATTR_TARGET_TEMP_HIGH],
unit, TEMP_CELSIUS), 1)
response['thermostatTemperatureSetpointLow'] = \
round(temp_util.convert(
attrs[climate.ATTR_TARGET_TEMP_LOW],
unit, TEMP_CELSIUS), 1)
current_temp,
unit,
TEMP_CELSIUS
), 1)
current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
if current_humidity is not None:
response['thermostatHumidityAmbient'] = current_humidity
if operation == climate.STATE_AUTO:
if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
response['thermostatTemperatureSetpointHigh'] = \
round(temp_util.convert(
attrs[climate.ATTR_TARGET_TEMP_HIGH],
unit, TEMP_CELSIUS), 1)
response['thermostatTemperatureSetpointLow'] = \
round(temp_util.convert(
attrs[climate.ATTR_TARGET_TEMP_LOW],
unit, TEMP_CELSIUS), 1)
else:
target_temp = attrs.get(ATTR_TEMPERATURE)
if target_temp is not None:
target_temp = round(
temp_util.convert(
target_temp,
unit,
TEMP_CELSIUS
), 1)
response['thermostatTemperatureSetpointHigh'] = \
target_temp
response['thermostatTemperatureSetpointLow'] = \
target_temp
else:
target_temp = attrs.get(ATTR_TEMPERATURE)
if target_temp is not None:
target_temp = round(
response['thermostatTemperatureSetpoint'] = round(
temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
response['thermostatTemperatureSetpointHigh'] = target_temp
response['thermostatTemperatureSetpointLow'] = target_temp
else:
target_temp = attrs.get(ATTR_TEMPERATURE)
if target_temp is not None:
response['thermostatTemperatureSetpoint'] = round(
temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
return response
async def execute(self, command, data, params, challenge):
"""Execute a temperature point or mode command."""
domain = self.state.domain
if domain == sensor.DOMAIN:
raise SmartHomeError(
ERR_NOT_SUPPORTED,
'Execute is not supported by sensor')
# All sent in temperatures are always in Celsius
unit = self.hass.config.units.temperature_unit
min_temp = self.state.attributes[climate.ATTR_MIN_TEMP]
@@ -687,8 +725,8 @@ class TemperatureSettingTrait(_Trait):
ATTR_ENTITY_ID: self.state.entity_id,
}
if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH
and supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
svc_data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
else:
+10 -8
View File
@@ -11,19 +11,21 @@ from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, \
from homeassistant.helpers import config_entry_flow
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
from .const import DOMAIN
from .const import (
DOMAIN,
ATTR_ALTITUDE,
ATTR_ACCURACY,
ATTR_ACTIVITY,
ATTR_DEVICE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
)
_LOGGER = logging.getLogger(__name__)
TRACKER_UPDATE = '{}_tracker_update'.format(DOMAIN)
ATTR_ALTITUDE = 'altitude'
ATTR_ACCURACY = 'accuracy'
ATTR_ACTIVITY = 'activity'
ATTR_DEVICE = 'device'
ATTR_DIRECTION = 'direction'
ATTR_PROVIDER = 'provider'
ATTR_SPEED = 'speed'
DEFAULT_ACCURACY = 200
DEFAULT_BATTERY = -1
@@ -1,3 +1,11 @@
"""Const for GPSLogger."""
DOMAIN = 'gpslogger'
ATTR_ALTITUDE = 'altitude'
ATTR_ACCURACY = 'accuracy'
ATTR_ACTIVITY = 'activity'
ATTR_DEVICE = 'device'
ATTR_DIRECTION = 'direction'
ATTR_PROVIDER = 'provider'
ATTR_SPEED = 'speed'
@@ -2,14 +2,29 @@
import logging
from homeassistant.core import callback
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
)
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import (
DeviceTrackerEntity
)
from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE
from .const import (
ATTR_ACTIVITY,
ATTR_ALTITUDE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
)
_LOGGER = logging.getLogger(__name__)
@@ -32,8 +47,27 @@ async def async_setup_entry(hass: HomeAssistantType, entry,
hass.data[GPL_DOMAIN]['unsub_device_tracker'][entry.entry_id] = \
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
# Restore previously loaded devices
dev_reg = await device_registry.async_get_registry(hass)
dev_ids = {
identifier[1]
for device in dev_reg.devices.values()
for identifier in device.identifiers
if identifier[0] == GPL_DOMAIN
}
if not dev_ids:
return
class GPSLoggerEntity(DeviceTrackerEntity):
entities = []
for dev_id in dev_ids:
hass.data[GPL_DOMAIN]['devices'].add(dev_id)
entity = GPSLoggerEntity(dev_id, None, None, None, None)
entities.append(entity)
async_add_entities(entities)
class GPSLoggerEntity(DeviceTrackerEntity, RestoreEntity):
"""Represent a tracked device."""
def __init__(
@@ -102,11 +136,46 @@ class GPSLoggerEntity(DeviceTrackerEntity):
async def async_added_to_hass(self):
"""Register state update callback."""
await super().async_added_to_hass()
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, TRACKER_UPDATE, self._async_receive_data)
# don't restore if we got created with data
if self._location is not None:
return
state = await self.async_get_last_state()
if state is None:
self._location = (None, None)
self._accuracy = None
self._attributes = {
ATTR_ALTITUDE: None,
ATTR_ACTIVITY: None,
ATTR_DIRECTION: None,
ATTR_PROVIDER: None,
ATTR_SPEED: None,
}
self._battery = None
return
attr = state.attributes
self._location = (
attr.get(ATTR_LATITUDE),
attr.get(ATTR_LONGITUDE),
)
self._accuracy = attr.get(ATTR_GPS_ACCURACY)
self._attributes = {
ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE),
ATTR_ACTIVITY: attr.get(ATTR_ACTIVITY),
ATTR_DIRECTION: attr.get(ATTR_DIRECTION),
ATTR_PROVIDER: attr.get(ATTR_PROVIDER),
ATTR_SPEED: attr.get(ATTR_SPEED),
}
self._battery = attr.get(ATTR_BATTERY_LEVEL)
async def async_will_remove_from_hass(self):
"""Clean up after entity before removal."""
await super().async_will_remove_from_hass()
self._unsub_dispatcher()
@callback
+9 -3
View File
@@ -9,7 +9,8 @@ from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG
import homeassistant.config as conf_util
from homeassistant.const import (
ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP)
ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP,
EVENT_CORE_CONFIG_UPDATE)
from homeassistant.core import DOMAIN as HASS_DOMAIN, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
@@ -194,8 +195,13 @@ async def async_setup(hass, config):
await hassio.update_hass_api(config.get('http', {}), refresh_token.token)
if 'homeassistant' in config:
await hassio.update_hass_timezone(config['homeassistant'])
async def push_config(_):
"""Push core config to Hass.io."""
await hassio.update_hass_timezone(str(hass.config.time_zone))
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
await push_config(None)
async def async_service_handler(service):
"""Handle service calls for Hass.io."""
+3 -3
View File
@@ -11,7 +11,7 @@ from homeassistant.components.http import (
CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE,
)
from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT
from homeassistant.const import SERVER_PORT
from .const import X_HASSIO
@@ -140,13 +140,13 @@ class HassIO:
payload=options)
@_api_bool
def update_hass_timezone(self, core_config):
def update_hass_timezone(self, timezone):
"""Update Home-Assistant timezone data on Hass.io.
This method return a coroutine.
"""
return self.send_command("/supervisor/options", payload={
'timezone': core_config.get(CONF_TIME_ZONE)
'timezone': timezone
})
async def send_command(self, command, method="post", payload=None,
@@ -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'
@@ -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
+5
View File
@@ -1,4 +1,5 @@
"""Provide CORS support for the HTTP component."""
from aiohttp.web_urldispatcher import Resource, ResourceRoute
from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION
from homeassistant.const import (
@@ -8,6 +9,7 @@ from homeassistant.core import callback
ALLOWED_CORS_HEADERS = [
ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
HTTP_HEADER_HA_AUTH, AUTHORIZATION]
VALID_CORS_TYPES = (Resource, ResourceRoute)
@callback
@@ -31,6 +33,9 @@ def setup_cors(app, origins):
else:
path = route
if not isinstance(path, VALID_CORS_TYPES):
return
path = path.canonical
if path in cors_added:
@@ -15,6 +15,8 @@ from .bridge import get_bridge
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
HUE_MANUFACTURERURL = 'http://www.philips.com'
@callback
def configured_hosts(hass):
@@ -143,6 +145,11 @@ class HueFlowHandler(config_entries.ConfigFlow):
This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not.
"""
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
return self.async_abort(reason='not_hue_bridge')
# Filter out emulated Hue
if "HASS Bridge" in discovery_info.get('name', ''):
return self.async_abort(reason='already_configured')
@@ -169,6 +176,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.
@@ -11,6 +11,11 @@
"Royal Philips Electronics"
]
},
"homekit": {
"models": [
"BSB002"
]
},
"dependencies": [],
"codeowners": [
"@balloob"
+2 -1
View File
@@ -24,7 +24,8 @@
"unknown": "Unknown error occurred",
"cannot_connect": "Unable to connect to the bridge",
"already_configured": "Bridge is already configured",
"already_in_progress": "Config flow for bridge is already in progress."
"already_in_progress": "Config flow for bridge is already in progress.",
"not_hue_bridge": "Not a Hue bridge"
}
}
}
@@ -145,9 +145,9 @@ class MobileAppEntity(DeviceTrackerEntity, RestoreEntity):
attr = state.attributes
data = {
ATTR_GPS: (attr[ATTR_LATITUDE], attr[ATTR_LONGITUDE]),
ATTR_GPS_ACCURACY: attr[ATTR_GPS_ACCURACY],
ATTR_BATTERY: attr[ATTR_BATTERY_LEVEL],
ATTR_GPS: (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)),
ATTR_GPS_ACCURACY: attr.get(ATTR_GPS_ACCURACY),
ATTR_BATTERY: attr.get(ATTR_BATTERY_LEVEL),
}
data.update({key: attr[key] for key in attr if key in ATTR_KEYS})
self._data = data
+11 -1
View File
@@ -192,6 +192,7 @@ class OwnTracksContext:
self.region_mapping = region_mapping
self.events_only = events_only
self.mqtt_topic = mqtt_topic
self._pending_msg = []
@callback
def async_valid_accuracy(self, message):
@@ -222,10 +223,19 @@ class OwnTracksContext:
return True
@callback
def set_async_see(self, func):
"""Set a new async_see function."""
self.async_see = func
for msg in self._pending_msg:
func(**msg)
self._pending_msg.clear()
# pylint: disable=method-hidden
@callback
def async_see(self, **data):
"""Send a see message to the device tracker."""
raise NotImplementedError
self._pending_msg.append(data)
@callback
def async_see_beacons(self, hass, dev_id, kwargs_param):
@@ -36,7 +36,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
async_add_entities([entity])
hass.data[OT_DOMAIN]['context'].async_see = _receive_data
hass.data[OT_DOMAIN]['context'].set_async_see(_receive_data)
# Restore previously loaded devices
dev_reg = await device_registry.async_get_registry(hass)
@@ -153,10 +153,10 @@ class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity):
attr = state.attributes
self._data = {
'host_name': state.name,
'gps': (attr[ATTR_LATITUDE], attr[ATTR_LONGITUDE]),
'gps_accuracy': attr[ATTR_GPS_ACCURACY],
'battery': attr[ATTR_BATTERY_LEVEL],
'source_type': attr[ATTR_SOURCE_TYPE],
'gps': (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)),
'gps_accuracy': attr.get(ATTR_GPS_ACCURACY),
'battery': attr.get(ATTR_BATTERY_LEVEL),
'source_type': attr.get(ATTR_SOURCE_TYPE),
}
@callback
+8 -3
View File
@@ -23,6 +23,7 @@ ATTR_MODEL_NAME = 'model_name'
ATTR_MODEL_NUMBER = 'model_number'
ATTR_SERIAL = 'serial_number'
ATTR_MANUFACTURER = 'manufacturer'
ATTR_MANUFACTURERURL = 'manufacturerURL'
ATTR_UDN = 'udn'
ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type'
@@ -85,13 +86,16 @@ class Scanner:
if not to_load:
return
for entry, info, domains in to_load:
tasks = []
for entry, info, domains in to_load:
for domain in domains:
_LOGGER.debug("Discovered %s at %s", domain, entry.location)
await self.hass.config_entries.flow.async_init(
tasks.append(self.hass.config_entries.flow.async_init(
domain, context={'source': DOMAIN}, data=info
)
))
await asyncio.wait(tasks)
async def _process_entry(self, entry):
"""Process a single entry."""
@@ -164,6 +168,7 @@ def info_from_entry(entry, device_info):
info[ATTR_MODEL_NUMBER] = device_info.get('modelNumber')
info[ATTR_SERIAL] = device_info.get('serialNumber')
info[ATTR_MANUFACTURER] = device_info.get('manufacturer')
info[ATTR_MANUFACTURERURL] = device_info.get('manufacturerURL')
info[ATTR_UDN] = device_info.get('UDN')
info[ATTR_UPNP_DEVICE_TYPE] = device_info.get('deviceType')
+14 -7
View File
@@ -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
@@ -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:
@@ -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():
@@ -6,6 +6,11 @@
"requirements": [
"pytradfri[async]==6.0.1"
],
"homekit": {
"models": [
"TRADFRI"
]
},
"dependencies": [],
"zeroconf": ["_coap._udp.local."],
"codeowners": [
+20 -5
View File
@@ -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
@@ -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",
"name": "Wemo",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/wemo",
"requirements": [
"pywemo==0.4.34"
],
"ssdp": {
"manufacturer": [
"Belkin International Inc."
]
},
"homekit": {
"models": [
"Wemo"
]
},
"dependencies": [],
"codeowners": [
"@sqldiablo"
@@ -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."
}
}
}
+9 -1
View File
@@ -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."""
+1 -1
View File
@@ -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"],
+14 -4
View File
@@ -3,12 +3,14 @@
# https://github.com/PyCQA/pylint/issues/1931
# pylint: disable=no-name-in-module
import logging
import socket
import ipaddress
import voluptuous as vol
from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf
from homeassistant import util
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__)
from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT
@@ -42,8 +44,16 @@ def setup(hass, config):
'requires_api_password': True,
}
info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name,
port=hass.http.server_port, properties=params)
host_ip = util.get_local_ip()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
except socket.error:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, None,
addresses=[host_ip_pton], port=hass.http.server_port,
properties=params)
zeroconf = Zeroconf()
@@ -102,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(
@@ -127,7 +137,7 @@ def info_from_service(service):
except UnicodeDecodeError:
_LOGGER.warning("Unicode decode error on %s: %s", key, value)
address = service.address or service.address6
address = service.addresses[0]
info = {
ATTR_HOST: str(ipaddress.ip_address(address)),
@@ -3,7 +3,7 @@
"name": "Zeroconf",
"documentation": "https://www.home-assistant.io/components/zeroconf",
"requirements": [
"zeroconf==0.22.0"
"zeroconf==0.23.0"
],
"dependencies": [
"api"
+21 -7
View File
@@ -3,10 +3,12 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.loader import bind_hass
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS)
CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS,
EVENT_CORE_CONFIG_UPDATE)
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util import slugify
@@ -90,12 +92,24 @@ async def async_setup(hass, config):
hass.async_create_task(zone.async_update_ha_state())
entities.add(zone.entity_id)
if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries:
zone = Zone(hass, hass.config.location_name,
hass.config.latitude, hass.config.longitude,
DEFAULT_RADIUS, ICON_HOME, False)
zone.entity_id = ENTITY_ID_HOME
hass.async_create_task(zone.async_update_ha_state())
if ENTITY_ID_HOME in entities or HOME_ZONE in zone_entries:
return True
zone = Zone(hass, hass.config.location_name,
hass.config.latitude, hass.config.longitude,
DEFAULT_RADIUS, ICON_HOME, False)
zone.entity_id = ENTITY_ID_HOME
hass.async_create_task(zone.async_update_ha_state())
@callback
def core_config_updated(_):
"""Handle core config updated."""
zone.name = hass.config.location_name
zone.latitude = hass.config.latitude
zone.longitude = hass.config.longitude
zone.async_write_ha_state()
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)
return True
+7 -10
View File
@@ -23,21 +23,18 @@ def in_zone(zone, latitude, longitude, radius=0) -> bool:
class Zone(Entity):
"""Representation of a Zone."""
name = None
def __init__(self, hass, name, latitude, longitude, radius, icon, passive):
"""Initialize the zone."""
self.hass = hass
self._name = name
self._latitude = latitude
self._longitude = longitude
self.name = name
self.latitude = latitude
self.longitude = longitude
self._radius = radius
self._icon = icon
self._passive = passive
@property
def name(self):
"""Return the name of the zone."""
return self._name
@property
def state(self):
"""Return the state property really does nothing for a zone."""
@@ -53,8 +50,8 @@ class Zone(Entity):
"""Return the state attributes of the zone."""
data = {
ATTR_HIDDEN: True,
ATTR_LATITUDE: self._latitude,
ATTR_LONGITUDE: self._longitude,
ATTR_LATITUDE: self.latitude,
ATTR_LONGITUDE: self.longitude,
ATTR_RADIUS: self._radius,
}
if self._passive:
+13 -3
View File
@@ -1,7 +1,7 @@
"""Module to help with parsing and generating configuration files."""
from collections import OrderedDict
# pylint: disable=no-name-in-module
from distutils.version import LooseVersion # pylint: disable=import-error
from distutils.version import StrictVersion # pylint: disable=import-error
import logging
import os
import re
@@ -31,6 +31,7 @@ from homeassistant.loader import (
Integration, async_get_integration, IntegrationNotFound
)
from homeassistant.util.yaml import load_yaml, SECRET_YAML
from homeassistant.util.package import is_docker_env
import homeassistant.helpers.config_validation as cv
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
from homeassistant.helpers.entity_values import EntityValues
@@ -333,13 +334,15 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
_LOGGER.info("Upgrading configuration directory from %s to %s",
conf_version, __version__)
if LooseVersion(conf_version) < LooseVersion('0.50'):
version_obj = StrictVersion(conf_version)
if version_obj < StrictVersion('0.50'):
# 0.50 introduced persistent deps dir.
lib_path = hass.config.path('deps')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
if LooseVersion(conf_version) < LooseVersion('0.92'):
if version_obj < StrictVersion('0.92'):
# 0.92 moved google/tts.py to google_translate/tts.py
config_path = find_config_file(hass.config.config_dir)
assert config_path is not None
@@ -357,6 +360,13 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
_LOGGER.exception("Migrating to google_translate tts failed")
pass
if version_obj < StrictVersion('0.94.0b6') and is_docker_env():
# In 0.94 we no longer install packages inside the deps folder when
# running inside a Docker container.
lib_path = hass.config.path('deps')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
with open(version_path, 'wt') as outp:
outp.write(__version__)
+1 -1
View File
@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 94
PATCH_VERSION = '0b5'
PATCH_VERSION = '2'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
+3 -9
View File
@@ -1288,10 +1288,7 @@ class Config:
unit_system: Optional[str] = None,
location_name: Optional[str] = None,
time_zone: Optional[str] = None) -> None:
"""Update the configuration from a dictionary.
Async friendly.
"""
"""Update the configuration from a dictionary."""
self.config_source = source
if latitude is not None:
self.latitude = latitude
@@ -1309,11 +1306,8 @@ class Config:
if time_zone is not None:
self.set_time_zone(time_zone)
async def update(self, **kwargs: Any) -> None:
"""Update the configuration from a dictionary.
Async friendly.
"""
async def async_update(self, **kwargs: Any) -> None:
"""Update the configuration from a dictionary."""
self._update(source=SOURCE_STORAGE, **kwargs)
await self.async_store()
self.hass.bus.async_fire(
+1
View File
@@ -49,6 +49,7 @@ FLOWS = [
"twilio",
"unifi",
"upnp",
"wemo",
"zha",
"zone",
"zwave"
+4
View File
@@ -7,7 +7,11 @@ To update, run python3 -m hassfest
SSDP = {
"device_type": {},
"manufacturer": {
"Belkin International Inc.": [
"wemo"
],
"Royal Philips Electronics": [
"deconz",
"hue"
]
},
+4 -1
View File
@@ -20,5 +20,8 @@ ZEROCONF = {
}
HOMEKIT = {
"LIFX ": "lifx"
"BSB002": "hue",
"LIFX": "lifx",
"TRADFRI": "tradfri",
"Wemo": "wemo"
}
+72 -45
View File
@@ -1,15 +1,18 @@
"""Helpers for listening to events."""
from datetime import timedelta
import functools as ft
from typing import Callable
import attr
from homeassistant.loader import bind_hass
from homeassistant.helpers.sun import get_astral_event_next
from ..core import HomeAssistant, callback
from ..const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (
ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL,
SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET)
from ..util import dt as dt_util
from ..util.async_ import run_callback_threadsafe
SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, EVENT_CORE_CONFIG_UPDATE)
from homeassistant.util import dt as dt_util
from homeassistant.util.async_ import run_callback_threadsafe
# PyLint does not like the use of threaded_listener_factory
# pylint: disable=invalid-name
@@ -263,30 +266,71 @@ def async_track_time_interval(hass, action, interval):
track_time_interval = threaded_listener_factory(async_track_time_interval)
@attr.s
class SunListener:
"""Helper class to help listen to sun events."""
hass = attr.ib(type=HomeAssistant)
action = attr.ib(type=Callable)
event = attr.ib(type=str)
offset = attr.ib(type=timedelta)
_unsub_sun = attr.ib(default=None)
_unsub_config = attr.ib(default=None)
@callback
def async_attach(self):
"""Attach a sun listener."""
assert self._unsub_config is None
self._unsub_config = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self._handle_config_event)
self._listen_next_sun_event()
@callback
def async_detach(self):
"""Detach the sun listener."""
assert self._unsub_sun is not None
assert self._unsub_config is not None
self._unsub_sun()
self._unsub_sun = None
self._unsub_config()
self._unsub_config = None
@callback
def _listen_next_sun_event(self):
"""Set up the sun event listener."""
assert self._unsub_sun is None
self._unsub_sun = async_track_point_in_utc_time(
self.hass, self._handle_sun_event,
get_astral_event_next(self.hass, self.event, offset=self.offset)
)
@callback
def _handle_sun_event(self, _now):
"""Handle solar event."""
self._unsub_sun = None
self._listen_next_sun_event()
self.hass.async_run_job(self.action)
@callback
def _handle_config_event(self, _event):
"""Handle core config update."""
assert self._unsub_sun is not None
self._unsub_sun()
self._unsub_sun = None
self._listen_next_sun_event()
@callback
@bind_hass
def async_track_sunrise(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunrise daily."""
remove = None
@callback
def sunrise_automation_listener(now):
"""Handle points in time to execute actions."""
nonlocal remove
remove = async_track_point_in_utc_time(
hass, sunrise_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNRISE, offset=offset))
hass.async_run_job(action)
remove = async_track_point_in_utc_time(
hass, sunrise_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNRISE, offset=offset))
def remove_listener():
"""Remove sunset listener."""
remove()
return remove_listener
listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset)
listener.async_attach()
return listener.async_detach
track_sunrise = threaded_listener_factory(async_track_sunrise)
@@ -296,26 +340,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise)
@bind_hass
def async_track_sunset(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunset daily."""
remove = None
@callback
def sunset_automation_listener(now):
"""Handle points in time to execute actions."""
nonlocal remove
remove = async_track_point_in_utc_time(
hass, sunset_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNSET, offset=offset))
hass.async_run_job(action)
remove = async_track_point_in_utc_time(
hass, sunset_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNSET, offset=offset))
def remove_listener():
"""Remove sunset listener."""
remove()
return remove_listener
listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset)
listener.async_attach()
return listener.async_detach
track_sunset = threaded_listener_factory(async_track_sunset)
+16 -4
View File
@@ -1,6 +1,6 @@
"""Module to handle installing requirements."""
import asyncio
from functools import partial
from pathlib import Path
import logging
import os
from typing import Any, Dict, List, Optional
@@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant
DATA_PIP_LOCK = 'pip_lock'
DATA_PKG_CACHE = 'pkg_cache'
CONSTRAINT_FILE = 'package_constraints.txt'
PROGRESS_FILE = '.pip_progress'
_LOGGER = logging.getLogger(__name__)
@@ -24,15 +25,16 @@ async def async_process_requirements(hass: HomeAssistant, name: str,
if pip_lock is None:
pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock()
pip_install = partial(pkg_util.install_package,
**pip_kwargs(hass.config.config_dir))
kwargs = pip_kwargs(hass.config.config_dir)
async with pip_lock:
for req in requirements:
if pkg_util.is_installed(req):
continue
ret = await hass.async_add_executor_job(pip_install, req)
ret = await hass.async_add_executor_job(
_install, hass, req, kwargs
)
if not ret:
_LOGGER.error("Not initializing %s because could not install "
@@ -42,6 +44,16 @@ async def async_process_requirements(hass: HomeAssistant, name: str,
return True
def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool:
"""Install requirement."""
progress_path = Path(hass.config.path(PROGRESS_FILE))
progress_path.touch()
try:
return pkg_util.install_package(req, **kwargs)
finally:
progress_path.unlink()
def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
"""Return keyword arguments for PIP install."""
is_docker = pkg_util.is_docker_env()
+6 -6
View File
@@ -126,7 +126,7 @@ aiobotocore==0.10.2
aiodns==2.0.0
# homeassistant.components.esphome
aioesphomeapi==2.0.1
aioesphomeapi==2.1.0
# homeassistant.components.freebox
aiofreepybox==0.0.8
@@ -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
@@ -577,7 +577,7 @@ hole==0.3.0
holidays==0.9.10
# homeassistant.components.frontend
home-assistant-frontend==20190602.0
home-assistant-frontend==20190604.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.4
@@ -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
@@ -1045,7 +1045,7 @@ pydaikin==1.4.6
pydanfossair==0.1.0
# homeassistant.components.deconz
pydeconz==59
pydeconz==60
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -1875,7 +1875,7 @@ youtube_dl==2019.05.11
zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.22.0
zeroconf==0.23.0
# homeassistant.components.zha
zha-quirks==0.0.13
+5 -5
View File
@@ -45,7 +45,7 @@ aioautomatic==0.6.5
aiobotocore==0.10.2
# homeassistant.components.esphome
aioesphomeapi==2.0.1
aioesphomeapi==2.1.0
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -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
@@ -148,7 +148,7 @@ hdate==0.8.7
holidays==0.9.10
# homeassistant.components.frontend
home-assistant-frontend==20190602.0
home-assistant-frontend==20190604.0
# homeassistant.components.homekit_controller
homekit[IP]==0.14.0
@@ -230,7 +230,7 @@ pyHS100==0.3.5
pyblackbird==0.5
# homeassistant.components.deconz
pydeconz==59
pydeconz==60
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -352,7 +352,7 @@ vultr==0.1.2
wakeonlan==1.1.6
# homeassistant.components.zeroconf
zeroconf==0.22.0
zeroconf==0.23.0
# homeassistant.components.zha
zigpy-homeassistant==0.3.3
+3 -1
View File
@@ -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
+2 -5
View File
@@ -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',
@@ -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()
+11 -11
View File
@@ -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()
+26 -8
View File
@@ -169,7 +169,7 @@ async def test_zeroconf_flow(hass):
data={
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80,
'properties': {'macaddress': '1234'}
'properties': {'macaddress': '00408C12345'}
},
context={'source': 'zeroconf'}
)
@@ -184,7 +184,7 @@ async def test_zeroconf_flow_known_device(hass):
This is legacy support from devices registered with configurator.
"""
with patch('homeassistant.components.axis.config_flow.load_json',
return_value={'1234ABCD': {
return_value={'00408C12345': {
config_flow.CONF_HOST: '2.3.4.5',
config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass',
@@ -208,7 +208,7 @@ async def test_zeroconf_flow_known_device(hass):
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80,
'hostname': 'name',
'properties': {'macaddress': '1234ABCD'}
'properties': {'macaddress': '00408C12345'}
},
context={'source': 'zeroconf'}
)
@@ -221,7 +221,7 @@ async def test_zeroconf_flow_already_configured(hass):
entry = MockConfigEntry(
domain=axis.DOMAIN,
data={axis.CONF_DEVICE: {axis.config_flow.CONF_HOST: '1.2.3.4'},
axis.config_flow.CONF_MAC: '1234ABCD'}
axis.config_flow.CONF_MAC: '00408C12345'}
)
entry.add_to_hass(hass)
@@ -233,7 +233,7 @@ async def test_zeroconf_flow_already_configured(hass):
config_flow.CONF_PASSWORD: 'pass',
config_flow.CONF_PORT: 80,
'hostname': 'name',
'properties': {'macaddress': '1234ABCD'}
'properties': {'macaddress': '00408C12345'}
},
context={'source': 'zeroconf'}
)
@@ -242,11 +242,29 @@ async def test_zeroconf_flow_already_configured(hass):
assert result['reason'] == 'already_configured'
async def test_zeroconf_flow_ignore_non_axis_device(hass):
"""Test that zeroconf doesn't setup devices with link local addresses."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: '169.254.3.4',
'properties': {'macaddress': '01234567890'}
},
context={'source': 'zeroconf'}
)
assert result['type'] == 'abort'
assert result['reason'] == 'not_axis_device'
async def test_zeroconf_flow_ignore_link_local_address(hass):
"""Test that zeroconf doesn't setup devices with link local addresses."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={config_flow.CONF_HOST: '169.254.3.4'},
data={
config_flow.CONF_HOST: '169.254.3.4',
'properties': {'macaddress': '00408C12345'}
},
context={'source': 'zeroconf'}
)
@@ -257,7 +275,7 @@ async def test_zeroconf_flow_ignore_link_local_address(hass):
async def test_zeroconf_flow_bad_config_file(hass):
"""Test that zeroconf discovery with bad config files abort."""
with patch('homeassistant.components.axis.config_flow.load_json',
return_value={'1234ABCD': {
return_value={'00408C12345': {
config_flow.CONF_HOST: '2.3.4.5',
config_flow.CONF_USERNAME: 'user',
config_flow.CONF_PASSWORD: 'pass',
@@ -268,7 +286,7 @@ async def test_zeroconf_flow_bad_config_file(hass):
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: '1.2.3.4',
'properties': {'macaddress': '1234ABCD'}
'properties': {'macaddress': '00408C12345'}
},
context={'source': 'zeroconf'}
)
+52 -1
View File
@@ -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
+33 -7
View File
@@ -1,9 +1,10 @@
"""Tests for deCONZ config flow."""
from unittest.mock import patch
from unittest.mock import Mock, patch
import asyncio
from homeassistant.components.deconz import config_flow
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from tests.common import MockConfigEntry
import pydeconz
@@ -168,22 +169,39 @@ async def test_link_no_api_key(hass):
assert result['errors'] == {'base': 'no_key'}
async def test_bridge_discovery(hass):
"""Test a bridge being discovered."""
async def test_bridge_ssdp_discovery(hass):
"""Test a bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80,
config_flow.CONF_SERIAL: 'id',
ATTR_SERIAL: 'id',
ATTR_MANUFACTURERURL:
config_flow.DECONZ_MANUFACTURERURL,
config_flow.ATTR_UUID: 'uuid:1234'
},
context={'source': 'discovery'}
context={'source': 'ssdp'}
)
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_bridge_ssdp_discovery_not_deconz_bridge(hass):
"""Test a non deconz bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={
ATTR_MANUFACTURERURL: 'not deconz bridge'
},
context={'source': 'ssdp'}
)
assert result['type'] == 'abort'
assert result['reason'] == 'not_deconz_bridge'
async def test_bridge_discovery_update_existing_entry(hass):
"""Test if a discovered bridge has already been configured."""
entry = MockConfigEntry(domain=config_flow.DOMAIN, data={
@@ -191,13 +209,21 @@ 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.CONF_SERIAL: 'id',
ATTR_SERIAL: 'id',
ATTR_MANUFACTURERURL:
config_flow.DECONZ_MANUFACTURERURL,
config_flow.ATTR_UUID: 'uuid:1234'
},
context={'source': 'discovery'}
context={'source': 'ssdp'}
)
assert result['type'] == 'abort'
@@ -14,6 +14,7 @@ from homeassistant.components import (
media_player,
scene,
script,
sensor,
switch,
vacuum,
group,
@@ -1380,3 +1381,35 @@ async def test_volume_media_player_relative(hass):
ATTR_ENTITY_ID: 'media_player.bla',
media_player.ATTR_MEDIA_VOLUME_LEVEL: .5
}
async def test_temperature_setting_sensor(hass):
"""Test TemperatureSetting trait support for temperature sensor."""
assert helpers.get_google_type(sensor.DOMAIN,
sensor.DEVICE_CLASS_TEMPERATURE) is not None
assert not trait.TemperatureSettingTrait.supported(
sensor.DOMAIN,
0,
sensor.DEVICE_CLASS_HUMIDITY
)
assert trait.TemperatureSettingTrait.supported(
sensor.DOMAIN,
0,
sensor.DEVICE_CLASS_TEMPERATURE
)
hass.config.units.temperature_unit = TEMP_FAHRENHEIT
trt = trait.TemperatureSettingTrait(hass, State('sensor.test', "70", {
ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE,
}), BASIC_CONFIG)
assert trt.sync_attributes() == {
'queryOnlyTemperatureSetting': True,
'thermostatTemperatureUnit': 'F',
}
assert trt.query_attributes() == {
'thermostatTemperatureAmbient': 21.1
}
hass.config.units.temperature_unit = TEMP_CELSIUS
+10 -5
View File
@@ -29,11 +29,16 @@ def hassio_env():
@pytest.fixture
def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock):
"""Create mock hassio http client."""
with patch('homeassistant.components.hassio.HassIO.update_hass_api',
Mock(return_value=mock_coro({"result": "ok"}))), \
patch('homeassistant.components.hassio.HassIO.'
'get_homeassistant_info',
Mock(side_effect=HassioAPIError())):
with patch(
'homeassistant.components.hassio.HassIO.update_hass_api',
return_value=mock_coro({"result": "ok"})
), patch(
'homeassistant.components.hassio.HassIO.update_hass_timezone',
return_value=mock_coro({"result": "ok"})
), patch(
'homeassistant.components.hassio.HassIO.get_homeassistant_info',
side_effect=HassioAPIError()
):
hass.state = CoreState.starting
hass.loop.run_until_complete(async_setup_component(hass, 'hassio', {
'http': {
+2 -2
View File
@@ -56,7 +56,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env):
})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 2
assert aioclient_mock.call_count == 3
assert mock_panel.called
mock_panel.assert_called_with(
hass, 'test1', {
@@ -98,7 +98,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env,
})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 2
assert aioclient_mock.call_count == 3
assert mock_panel.called
mock_panel.assert_called_with(
hass, 'test1', {
+21 -19
View File
@@ -43,7 +43,7 @@ def test_setup_api_ping(hass, aioclient_mock):
result = yield from async_setup_component(hass, 'hassio', {})
assert result
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
assert hass.components.hassio.get_homeassistant_version() == "10.0"
assert hass.components.hassio.is_hassio()
@@ -82,7 +82,7 @@ def test_setup_api_push_api_data(hass, aioclient_mock):
})
assert result
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 9999
assert aioclient_mock.mock_calls[1][2]['watchdog']
@@ -101,7 +101,7 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
})
assert result
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 9999
assert not aioclient_mock.mock_calls[1][2]['watchdog']
@@ -117,7 +117,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock,
})
assert result
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 8123
refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token']
@@ -177,27 +177,29 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock,
})
assert result
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
assert not aioclient_mock.mock_calls[1][2]['ssl']
assert aioclient_mock.mock_calls[1][2]['port'] == 8123
assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token
@asyncio.coroutine
def test_setup_core_push_timezone(hass, aioclient_mock):
async def test_setup_core_push_timezone(hass, aioclient_mock):
"""Test setup with API push default data."""
hass.config.time_zone = 'testzone'
with patch.dict(os.environ, MOCK_ENVIRON):
result = yield from async_setup_component(hass, 'hassio', {
result = await async_setup_component(hass, 'hassio', {
'hassio': {},
'homeassistant': {
'time_zone': 'testzone',
},
})
assert result
assert aioclient_mock.call_count == 5
assert aioclient_mock.mock_calls[2][2]['timezone'] == "testzone"
await hass.config.async_update(time_zone='America/New_York')
await hass.async_block_till_done()
assert aioclient_mock.mock_calls[-1][2]['timezone'] == "America/New_York"
@asyncio.coroutine
def test_setup_hassio_no_additional_data(hass, aioclient_mock):
@@ -209,7 +211,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock):
})
assert result
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456"
@@ -288,14 +290,14 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'})
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 6
assert aioclient_mock.call_count == 7
assert aioclient_mock.mock_calls[-1][2] == 'test'
yield from hass.services.async_call('hassio', 'host_shutdown', {})
yield from hass.services.async_call('hassio', 'host_reboot', {})
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 8
assert aioclient_mock.call_count == 9
yield from hass.services.async_call('hassio', 'snapshot_full', {})
yield from hass.services.async_call('hassio', 'snapshot_partial', {
@@ -305,7 +307,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
})
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 10
assert aioclient_mock.call_count == 11
assert aioclient_mock.mock_calls[-1][2] == {
'addons': ['test'], 'folders': ['ssl'], 'password': "123456"}
@@ -321,7 +323,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
})
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 12
assert aioclient_mock.call_count == 13
assert aioclient_mock.mock_calls[-1][2] == {
'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False,
'password': "123456"
@@ -341,12 +343,12 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock):
yield from hass.services.async_call('homeassistant', 'stop')
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 3
assert aioclient_mock.call_count == 4
yield from hass.services.async_call('homeassistant', 'check_config')
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 3
assert aioclient_mock.call_count == 4
with patch(
'homeassistant.config.async_check_ha_config_file',
@@ -356,4 +358,4 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock):
yield from hass.async_block_till_done()
assert mock_check_config.called
assert aioclient_mock.call_count == 4
assert aioclient_mock.call_count == 5
@@ -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,
+12
View File
@@ -140,3 +140,15 @@ async def test_cors_middleware_with_cors_allowed_view(hass):
hass.http.app._on_startup.freeze()
await hass.http.app.startup()
async def test_cors_works_with_frontend(hass, hass_client):
"""Test CORS works with the frontend."""
assert await async_setup_component(hass, 'frontend', {
'http': {
'cors_allowed_origins': ['http://home-assistant.io']
}
})
client = await hass_client()
resp = await client.get('/')
assert resp.status == 200
+53 -3
View File
@@ -195,13 +195,26 @@ async def test_bridge_ssdp(hass):
side_effect=errors.AuthenticationRequired):
result = await flow.async_step_ssdp({
'host': '0.0.0.0',
'serial': '1234'
'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
async def test_bridge_ssdp_discover_other_bridge(hass):
"""Test that discovery ignores other bridges."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
result = await flow.async_step_ssdp({
'manufacturerURL': 'http://www.notphilips.com'
})
assert result['type'] == 'abort'
async def test_bridge_ssdp_emulated_hue(hass):
"""Test if discovery info is from an emulated hue instance."""
flow = config_flow.HueFlowHandler()
@@ -211,7 +224,8 @@ async def test_bridge_ssdp_emulated_hue(hass):
result = await flow.async_step_ssdp({
'name': 'HASS Bridge',
'host': '0.0.0.0',
'serial': '1234'
'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})
assert result['type'] == 'abort'
@@ -229,7 +243,8 @@ async def test_bridge_ssdp_already_configured(hass):
result = await flow.async_step_ssdp({
'host': '0.0.0.0',
'serial': '1234'
'serial': '1234',
'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})
assert result['type'] == 'abort'
@@ -356,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'
+22 -1
View File
@@ -4,7 +4,7 @@ import asyncio
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components import owntracks
from tests.common import mock_component, MockConfigEntry
MINIMAL_LOCATION_MESSAGE = {
@@ -160,3 +160,24 @@ def test_returns_error_missing_device(mock_client):
json = yield from resp.json()
assert json == []
def test_context_delivers_pending_msg():
"""Test that context is able to hold pending messages while being init."""
context = owntracks.OwnTracksContext(
None, None, None, None, None, None, None, None
)
context.async_see(hello='world')
context.async_see(world='hello')
received = []
context.set_async_see(lambda **data: received.append(data))
assert len(received) == 2
assert received[0] == {'hello': 'world'}
assert received[1] == {'world': 'hello'}
received.clear()
context.set_async_see(lambda **data: received.append(data))
assert len(received) == 0
+10
View File
@@ -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):
+32 -7
View File
@@ -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'
+21
View File
@@ -221,3 +221,24 @@ class TestComponentZone(unittest.TestCase):
assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'),
latitude, longitude)
async def test_core_config_update(hass):
"""Test updating core config will update home zone."""
assert await setup.async_setup_component(hass, 'zone', {})
home = hass.states.get('zone.home')
await hass.config.async_update(
location_name='Updated Name',
latitude=10,
longitude=20,
)
await hass.async_block_till_done()
home_updated = hass.states.get('zone.home')
assert home is not home_updated
assert home_updated.name == 'Updated Name'
assert home_updated.attributes['latitude'] == 10
assert home_updated.attributes['longitude'] == 20
+62
View File
@@ -436,6 +436,68 @@ async def test_track_sunrise(hass):
assert len(offset_runs) == 1
async def test_track_sunrise_update_location(hass):
"""Test track the sunrise."""
# Setup sun component
hass.config.latitude = 32.87336
hass.config.longitude = 117.22743
assert await async_setup_component(hass, sun.DOMAIN, {
sun.DOMAIN: {sun.CONF_ELEVATION: 0}})
# Get next sunrise
astral = Astral()
utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC)
utc_today = utc_now.date()
mod = -1
while True:
next_rising = (astral.sunrise_utc(
utc_today + timedelta(days=mod),
hass.config.latitude, hass.config.longitude))
if next_rising > utc_now:
break
mod += 1
# Track sunrise
runs = []
with patch('homeassistant.util.dt.utcnow', return_value=utc_now):
async_track_sunrise(hass, lambda: runs.append(1))
# Mimick sunrise
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
assert len(runs) == 1
# Move!
with patch('homeassistant.util.dt.utcnow', return_value=utc_now):
await hass.config.async_update(
latitude=40.755931,
longitude=-73.984606,
)
await hass.async_block_till_done()
# Mimick sunrise
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
# Did not increase
assert len(runs) == 1
# Get next sunrise
mod = -1
while True:
next_rising = (astral.sunrise_utc(
utc_today + timedelta(days=mod),
hass.config.latitude, hass.config.longitude))
if next_rising > utc_now:
break
mod += 1
# Mimick sunrise at new location
_send_time_changed(hass, next_rising)
await hass.async_block_till_done()
assert len(runs) == 2
async def test_track_sunset(hass):
"""Test track the sunset."""
latitude = 32.87336
+25 -2
View File
@@ -256,7 +256,8 @@ async def test_entity_customization(hass):
@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass):
@mock.patch('homeassistant.config.is_docker_env', return_value=False)
def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass):
"""Test removal of library on upgrade from before 0.50."""
ha_version = '0.49.0'
mock_os.path.isdir = mock.Mock(return_value=True)
@@ -275,6 +276,28 @@ def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass):
assert mock_shutil.rmtree.call_args == mock.call(hass_path)
@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
@mock.patch('homeassistant.config.is_docker_env', return_value=True)
def test_remove_lib_on_upgrade_94(mock_docker, mock_os, mock_shutil, hass):
"""Test removal of library on upgrade from before 0.94 and in Docker."""
ha_version = '0.94.0b5'
mock_os.path.isdir = mock.Mock(return_value=True)
mock_open = mock.mock_open()
with mock.patch('homeassistant.config.open', mock_open, create=True):
opened_file = mock_open.return_value
# pylint: disable=no-member
opened_file.readline.return_value = ha_version
hass.config.path = mock.Mock()
config_util.process_ha_config_upgrade(hass)
hass_path = hass.config.path.return_value
assert mock_os.path.isdir.call_count == 1
assert mock_os.path.isdir.call_args == mock.call(hass_path)
assert mock_shutil.rmtree.call_count == 1
assert mock_shutil.rmtree.call_args == mock.call(hass_path)
def test_process_config_upgrade(hass):
"""Test update of version on upgrade."""
ha_version = '0.92.0'
@@ -421,7 +444,7 @@ async def test_updating_configuration(hass, hass_storage):
hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config(
hass, {'whitelist_external_dirs': '/tmp'})
await hass.config.update(latitude=50)
await hass.config.async_update(latitude=50)
new_core_data = copy.deepcopy(core_data)
new_core_data['data']['latitude'] = 50
+3 -3
View File
@@ -955,7 +955,7 @@ async def test_event_on_update(hass, hass_storage):
assert hass.config.latitude != 12
await hass.config.update(latitude=12)
await hass.config.async_update(latitude=12)
await hass.async_block_till_done()
assert hass.config.latitude == 12
@@ -963,10 +963,10 @@ async def test_event_on_update(hass, hass_storage):
assert events[0].data == {'latitude': 12}
def test_bad_timezone_raises_value_error(hass):
async def test_bad_timezone_raises_value_error(hass):
"""Test bad timezone raises ValueError."""
with pytest.raises(ValueError):
hass.config.set_time_zone('not_a_timezone')
await hass.config.async_update(time_zone='not_a_timezone')
@patch('homeassistant.core.monotonic')
+21 -1
View File
@@ -1,10 +1,11 @@
"""Test requirements module."""
import os
from pathlib import Path
from unittest.mock import patch, call
from homeassistant import setup
from homeassistant.requirements import (
CONSTRAINT_FILE, async_process_requirements)
CONSTRAINT_FILE, async_process_requirements, PROGRESS_FILE, _install)
from tests.common import (
get_test_home_assistant, MockModule, mock_coro, mock_integration)
@@ -143,3 +144,22 @@ async def test_install_on_docker(hass):
constraints=os.path.join('ha_package_path', CONSTRAINT_FILE),
no_cache_dir=True,
)
async def test_progress_lock(hass):
"""Test an install attempt on an existing package."""
progress_path = Path(hass.config.path(PROGRESS_FILE))
kwargs = {'hello': 'world'}
def assert_env(req, **passed_kwargs):
"""Assert the env."""
assert progress_path.exists()
assert req == 'hello'
assert passed_kwargs == kwargs
return True
with patch('homeassistant.util.package.install_package',
side_effect=assert_env):
_install(hass, 'hello', kwargs)
assert not progress_path.exists()