mirror of
https://github.com/home-assistant/core.git
synced 2026-01-24 16:42:38 +01:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d85ae5dcae | ||
|
|
4e0683565d | ||
|
|
4d6c07f18a | ||
|
|
b0c68e0ea7 | ||
|
|
2858f56d4d | ||
|
|
aa91211229 | ||
|
|
10c8f21f79 | ||
|
|
3da0f5e384 | ||
|
|
7260cada90 | ||
|
|
73d6dc6b6a | ||
|
|
c2218e8a64 | ||
|
|
6ea0575a4a | ||
|
|
d88d57f3bb | ||
|
|
bd80346592 | ||
|
|
7292f2be69 | ||
|
|
21d04b3e14 | ||
|
|
3c6235bee5 | ||
|
|
b0985bb459 | ||
|
|
8e93d0a7a2 | ||
|
|
282b4f4927 | ||
|
|
b68a796c7c | ||
|
|
bfafe9ccbe | ||
|
|
dc93779f02 | ||
|
|
14066dfb5a | ||
|
|
7d9988fd75 | ||
|
|
2fed016347 | ||
|
|
d1b82e9ede | ||
|
|
b8e20fcadf | ||
|
|
ebc09017b8 | ||
|
|
798b72e164 | ||
|
|
09292d5918 | ||
|
|
d78e132007 | ||
|
|
8d3c9bc2d0 | ||
|
|
ce93a332a7 | ||
|
|
13c3833593 | ||
|
|
d0715c75c0 | ||
|
|
eca424656a | ||
|
|
3b60081e2a | ||
|
|
1096fe3d87 | ||
|
|
389da16947 | ||
|
|
185af1b42a | ||
|
|
d17f27b65c | ||
|
|
bb0867f1a8 | ||
|
|
b67d32824c | ||
|
|
bad920fa87 | ||
|
|
281fe93a26 | ||
|
|
4a71593ffd | ||
|
|
014cc14b7e | ||
|
|
ee71d2ca60 | ||
|
|
5085ce8ab1 | ||
|
|
9ed5b70d01 | ||
|
|
a00d8a493d | ||
|
|
2b0e56932b | ||
|
|
704cdac874 | ||
|
|
89d7c0af91 | ||
|
|
5f3bcedbba | ||
|
|
d2d3f27f85 | ||
|
|
a8c73ffb93 | ||
|
|
22f68d70a7 | ||
|
|
bf85e18d45 | ||
|
|
09c43e8854 | ||
|
|
e5cbf01ce1 | ||
|
|
fe2e5089ab | ||
|
|
35ffac1e01 | ||
|
|
362f23a950 | ||
|
|
dc8d4ac8e4 | ||
|
|
0cdea28e2a | ||
|
|
7d1a02feb1 |
@@ -45,7 +45,7 @@ homeassistant/components/cisco_ios/* @fbradyirl
|
||||
homeassistant/components/cisco_mobility_express/* @fbradyirl
|
||||
homeassistant/components/cisco_webex_teams/* @fbradyirl
|
||||
homeassistant/components/ciscospark/* @fbradyirl
|
||||
homeassistant/components/cloud/* @home-assistant/core
|
||||
homeassistant/components/cloud/* @home-assistant/cloud
|
||||
homeassistant/components/cloudflare/* @ludeeus
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator/* @home-assistant/core
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,21 @@ 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
|
||||
|
||||
if any(serialnumber == flow['context']['macaddress']
|
||||
for flow in self._async_in_progress()):
|
||||
return self.async_abort(reason='already_in_progress')
|
||||
|
||||
device_entries = configured_devices(self.hass)
|
||||
|
||||
if serialnumber in device_entries:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Config flow for device is already in progress.",
|
||||
"device_unavailable": "Device is not available",
|
||||
"faulty_credentials": "Bad user credentials"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"name": "Cloud",
|
||||
"documentation": "https://www.home-assistant.io/components/cloud",
|
||||
"requirements": [
|
||||
"hass-nabucasa==0.13"
|
||||
"hass-nabucasa==0.14"
|
||||
],
|
||||
"dependencies": [
|
||||
"http",
|
||||
"webhook"
|
||||
],
|
||||
"codeowners": [
|
||||
"@home-assistant/core"
|
||||
"@home-assistant/cloud"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -56,6 +56,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
|
||||
self.context['title_placeholders'] = {
|
||||
'name': self._name
|
||||
}
|
||||
self.context['name'] = self._name
|
||||
|
||||
# Only show authentication step if device uses password
|
||||
if device_info.uses_password:
|
||||
@@ -98,9 +99,11 @@ class EsphomeFlowHandler(config_entries.ConfigFlow):
|
||||
already_configured = data.device_info.name == node_name
|
||||
|
||||
if already_configured:
|
||||
return self.async_abort(
|
||||
reason='already_configured'
|
||||
)
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
for flow in self._async_in_progress():
|
||||
if flow['context']['name'] == node_name:
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
return await self._async_authenticate_or_add(user_input={
|
||||
'host': address,
|
||||
|
||||
@@ -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==20190530.0"
|
||||
"home-assistant-frontend==20190604.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
"""Support for the Geofency device tracker platform."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||
from homeassistant.components.device_tracker.config_entry import (
|
||||
DeviceTrackerEntity
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers import device_registry
|
||||
|
||||
from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE
|
||||
|
||||
@@ -30,19 +36,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
hass.data[GF_DOMAIN]['unsub_device_tracker'][config_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] == GF_DOMAIN
|
||||
}
|
||||
|
||||
if dev_ids:
|
||||
hass.data[GF_DOMAIN]['devices'].update(dev_ids)
|
||||
async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GeofencyEntity(DeviceTrackerEntity):
|
||||
class GeofencyEntity(DeviceTrackerEntity, RestoreEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
def __init__(self, device, gps, location_name, attributes):
|
||||
def __init__(self, device, gps=None, location_name=None, attributes=None):
|
||||
"""Set up Geofency entity."""
|
||||
self._attributes = attributes
|
||||
self._attributes = attributes or {}
|
||||
self._name = device
|
||||
self._location_name = location_name
|
||||
self._gps = gps
|
||||
self._unsub_dispatcher = None
|
||||
self._unique_id = device
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
@@ -74,6 +94,19 @@ class GeofencyEntity(DeviceTrackerEntity):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return {
|
||||
'name': self._name,
|
||||
'identifiers': {(GF_DOMAIN, self._unique_id)},
|
||||
}
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
@@ -81,12 +114,27 @@ class GeofencyEntity(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)
|
||||
|
||||
if self._attributes:
|
||||
return
|
||||
|
||||
state = await self.async_get_last_state()
|
||||
|
||||
if state is None:
|
||||
self._gps = (None, None)
|
||||
return
|
||||
|
||||
attr = state.attributes
|
||||
self._gps = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Clean up after entity before removal."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._unsub_dispatcher()
|
||||
self.hass.data[GF_DOMAIN]['devices'].remove(self._unique_id)
|
||||
|
||||
@callback
|
||||
def _async_receive_data(self, device, gps, location_name, attributes):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__(
|
||||
@@ -45,6 +79,7 @@ class GPSLoggerEntity(DeviceTrackerEntity):
|
||||
self._battery = battery
|
||||
self._location = location
|
||||
self._unsub_dispatcher = None
|
||||
self._unique_id = device
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
@@ -81,6 +116,19 @@ class GPSLoggerEntity(DeviceTrackerEntity):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return {
|
||||
'name': self._name,
|
||||
'identifiers': {(GPL_DOMAIN, self._unique_id)},
|
||||
}
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
@@ -88,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,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."""
|
||||
|
||||
@@ -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'
|
||||
@@ -126,14 +124,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
# It changes if a device is factory reset.
|
||||
hkid = properties['id']
|
||||
model = properties['md']
|
||||
|
||||
name = discovery_info['name'].replace('._hap._tcp.local.', '')
|
||||
status_flags = int(properties['sf'])
|
||||
paired = not status_flags & 0x01
|
||||
|
||||
_LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid)
|
||||
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
self.context['hkid'] = hkid
|
||||
self.context['title_placeholders'] = {
|
||||
'name': discovery_info['name'].replace('._hap._tcp.local.', ''),
|
||||
'name': name,
|
||||
}
|
||||
|
||||
# If multiple HomekitControllerFlowHandler end up getting created
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,15 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
|
||||
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
|
||||
DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS,
|
||||
DATA_DEVICES, DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY,
|
||||
STORAGE_VERSION)
|
||||
DATA_DEVICES, DATA_SENSOR, DATA_STORE,
|
||||
DOMAIN, STORAGE_KEY, STORAGE_VERSION)
|
||||
|
||||
from .http_api import RegistrationsView
|
||||
from .webhook import handle_webhook
|
||||
from .websocket_api import register_websocket_handlers
|
||||
|
||||
PLATFORMS = 'sensor', 'binary_sensor', 'device_tracker'
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Set up the mobile app component."""
|
||||
@@ -24,7 +26,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
DATA_BINARY_SENSOR: {},
|
||||
DATA_CONFIG_ENTRIES: {},
|
||||
DATA_DELETED_IDS: [],
|
||||
DATA_DEVICES: {},
|
||||
DATA_SENSOR: {}
|
||||
}
|
||||
|
||||
@@ -83,10 +84,8 @@ async def async_setup_entry(hass, entry):
|
||||
webhook_register(hass, DOMAIN, registration_name, webhook_id,
|
||||
handle_webhook)
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry,
|
||||
DATA_BINARY_SENSOR))
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR))
|
||||
for domain in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, domain))
|
||||
|
||||
return True
|
||||
|
||||
@@ -160,6 +160,7 @@ SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR]
|
||||
COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES))
|
||||
|
||||
SIGNAL_SENSOR_UPDATE = DOMAIN + '_sensor_update'
|
||||
SIGNAL_LOCATION_UPDATE = DOMAIN + '_location_update_{}'
|
||||
|
||||
REGISTER_SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
|
||||
|
||||
167
homeassistant/components/mobile_app/device_tracker.py
Normal file
167
homeassistant/components/mobile_app/device_tracker.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Device tracker platform that adds support for OwnTracks over MQTT."""
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_BATTERY_LEVEL,
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
|
||||
from homeassistant.components.device_tracker.config_entry import (
|
||||
DeviceTrackerEntity
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .const import (
|
||||
ATTR_ALTITUDE,
|
||||
ATTR_BATTERY,
|
||||
ATTR_COURSE,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_DEVICE_NAME,
|
||||
ATTR_GPS_ACCURACY,
|
||||
ATTR_GPS,
|
||||
ATTR_LOCATION_NAME,
|
||||
ATTR_SPEED,
|
||||
ATTR_VERTICAL_ACCURACY,
|
||||
|
||||
SIGNAL_LOCATION_UPDATE,
|
||||
)
|
||||
from .helpers import device_info
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ATTR_KEYS = (
|
||||
ATTR_ALTITUDE,
|
||||
ATTR_COURSE,
|
||||
ATTR_SPEED,
|
||||
ATTR_VERTICAL_ACCURACY
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up OwnTracks based off an entry."""
|
||||
entity = MobileAppEntity(entry)
|
||||
async_add_entities([entity])
|
||||
return True
|
||||
|
||||
|
||||
class MobileAppEntity(DeviceTrackerEntity, RestoreEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
def __init__(self, entry, data=None):
|
||||
"""Set up OwnTracks entity."""
|
||||
self._entry = entry
|
||||
self._data = data
|
||||
self._dispatch_unsub = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self._entry.data[ATTR_DEVICE_ID]
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the battery level of the device."""
|
||||
return self._data.get(ATTR_BATTERY)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific attributes."""
|
||||
attrs = {}
|
||||
for key in ATTR_KEYS:
|
||||
value = self._data.get(key)
|
||||
if value is not None:
|
||||
attrs[key] = value
|
||||
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def location_accuracy(self):
|
||||
"""Return the gps accuracy of the device."""
|
||||
return self._data.get(ATTR_GPS_ACCURACY)
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
gps = self._data.get(ATTR_GPS)
|
||||
|
||||
if gps is None:
|
||||
return None
|
||||
|
||||
return gps[0]
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
gps = self._data.get(ATTR_GPS)
|
||||
|
||||
if gps is None:
|
||||
return None
|
||||
|
||||
return gps[1]
|
||||
|
||||
@property
|
||||
def location_name(self):
|
||||
"""Return a location name for the current location of the device."""
|
||||
return self._data.get(ATTR_LOCATION_NAME)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._entry.data[ATTR_DEVICE_NAME]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
return SOURCE_TYPE_GPS
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return device_info(self._entry.data)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
self._dispatch_unsub = \
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id),
|
||||
self.update_data
|
||||
)
|
||||
|
||||
# Don't restore if we got set up with data.
|
||||
if self._data is not None:
|
||||
return
|
||||
|
||||
state = await self.async_get_last_state()
|
||||
|
||||
if state is None:
|
||||
self._data = {}
|
||||
return
|
||||
|
||||
attr = state.attributes
|
||||
data = {
|
||||
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
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Call when entity is being removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
if self._dispatch_unsub:
|
||||
self._dispatch_unsub()
|
||||
self._dispatch_unsub = None
|
||||
|
||||
@callback
|
||||
def update_data(self, data):
|
||||
"""Mark the device as seen."""
|
||||
self._data = data
|
||||
self.async_write_ha_state()
|
||||
@@ -6,11 +6,11 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
|
||||
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES,
|
||||
from .const import (ATTR_SENSOR_ATTRIBUTES,
|
||||
ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON,
|
||||
ATTR_SENSOR_NAME, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID,
|
||||
DOMAIN, SIGNAL_SENSOR_UPDATE)
|
||||
from .helpers import device_info
|
||||
|
||||
|
||||
def sensor_id(webhook_id, unique_id):
|
||||
@@ -76,17 +76,7 @@ class MobileAppEntity(Entity):
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device registry information for this entity."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(ATTR_DEVICE_ID, self._registration[ATTR_DEVICE_ID]),
|
||||
(CONF_WEBHOOK_ID, self._registration[CONF_WEBHOOK_ID])
|
||||
},
|
||||
'manufacturer': self._registration[ATTR_MANUFACTURER],
|
||||
'model': self._registration[ATTR_MODEL],
|
||||
'device_name': self._registration[ATTR_DEVICE_NAME],
|
||||
'sw_version': self._registration[ATTR_OS_VERSION],
|
||||
'config_entries': self._device.config_entries
|
||||
}
|
||||
return device_info(self._registration)
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.core import Context
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
|
||||
from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_DEVICE_ID,
|
||||
ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
|
||||
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION,
|
||||
CONF_SECRET, CONF_USER_ID, DATA_BINARY_SENSOR,
|
||||
@@ -148,3 +148,16 @@ def webhook_response(data, *, registration: Dict, status: int = 200,
|
||||
|
||||
return Response(text=data, status=status, content_type='application/json',
|
||||
headers=headers)
|
||||
|
||||
|
||||
def device_info(registration: Dict) -> Dict:
|
||||
"""Return the device info for this registration."""
|
||||
return {
|
||||
'identifiers': {
|
||||
(DOMAIN, registration[ATTR_DEVICE_ID]),
|
||||
},
|
||||
'manufacturer': registration[ATTR_MANUFACTURER],
|
||||
'model': registration[ATTR_MODEL],
|
||||
'device_name': registration[ATTR_DEVICE_NAME],
|
||||
'sw_version': registration[ATTR_OS_VERSION],
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"PyNaCl==1.3.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"device_tracker",
|
||||
"http",
|
||||
"webhook"
|
||||
],
|
||||
|
||||
@@ -6,10 +6,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.cloud import (async_remote_ui_url,
|
||||
CloudNotAvailable)
|
||||
from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES,
|
||||
ATTR_DEV_ID,
|
||||
DOMAIN as DT_DOMAIN,
|
||||
SERVICE_SEE as DT_SEE)
|
||||
from homeassistant.components.frontend import MANIFEST_JSON
|
||||
from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN
|
||||
|
||||
@@ -24,15 +20,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.template import attach
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
|
||||
from .const import (ATTR_DEVICE_ID,
|
||||
ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE,
|
||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME,
|
||||
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
|
||||
ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SPEED,
|
||||
ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID,
|
||||
ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE,
|
||||
ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
|
||||
ATTR_TEMPLATE_VARIABLES,
|
||||
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
|
||||
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
|
||||
CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET,
|
||||
@@ -45,7 +38,7 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
|
||||
WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE,
|
||||
WEBHOOK_TYPE_UPDATE_LOCATION,
|
||||
WEBHOOK_TYPE_UPDATE_REGISTRATION,
|
||||
WEBHOOK_TYPE_UPDATE_SENSOR_STATES)
|
||||
WEBHOOK_TYPE_UPDATE_SENSOR_STATES, SIGNAL_LOCATION_UPDATE)
|
||||
|
||||
|
||||
from .helpers import (_decrypt_payload, empty_okay_response, error_response,
|
||||
@@ -151,37 +144,9 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
|
||||
headers=headers)
|
||||
|
||||
if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION:
|
||||
see_payload = {
|
||||
ATTR_DEV_ID: slugify(registration[ATTR_DEVICE_NAME]),
|
||||
ATTR_GPS: data[ATTR_GPS],
|
||||
ATTR_GPS_ACCURACY: data[ATTR_GPS_ACCURACY],
|
||||
}
|
||||
|
||||
for key in (ATTR_LOCATION_NAME, ATTR_BATTERY):
|
||||
value = data.get(key)
|
||||
if value is not None:
|
||||
see_payload[key] = value
|
||||
|
||||
attrs = {}
|
||||
|
||||
for key in (ATTR_ALTITUDE, ATTR_COURSE,
|
||||
ATTR_SPEED, ATTR_VERTICAL_ACCURACY):
|
||||
value = data.get(key)
|
||||
if value is not None:
|
||||
attrs[key] = value
|
||||
|
||||
if attrs:
|
||||
see_payload[ATTR_ATTRIBUTES] = attrs
|
||||
|
||||
try:
|
||||
await hass.services.async_call(DT_DOMAIN,
|
||||
DT_SEE, see_payload,
|
||||
blocking=True, context=context)
|
||||
# noqa: E722 pylint: disable=broad-except
|
||||
except (vol.Invalid, ServiceNotFound, Exception) as ex:
|
||||
_LOGGER.error("Error when updating location during mobile_app "
|
||||
"webhook (device name: %s): %s",
|
||||
registration[ATTR_DEVICE_NAME], ex)
|
||||
hass.helpers.dispatcher.async_dispatcher_send(
|
||||
SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data
|
||||
)
|
||||
return empty_okay_response(headers=headers)
|
||||
|
||||
if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT
|
||||
from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_BATTERY_LEVEL,
|
||||
)
|
||||
from homeassistant.components.device_tracker.const import (
|
||||
ENTITY_ID_FORMAT, ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS)
|
||||
from homeassistant.components.device_tracker.config_entry import (
|
||||
DeviceTrackerEntity
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers import device_registry
|
||||
from . import DOMAIN as OT_DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -14,53 +23,52 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up OwnTracks based off an entry."""
|
||||
@callback
|
||||
def _receive_data(dev_id, host_name, gps, attributes, gps_accuracy=None,
|
||||
battery=None, source_type=None, location_name=None):
|
||||
def _receive_data(dev_id, **data):
|
||||
"""Receive set location."""
|
||||
device = hass.data[OT_DOMAIN]['devices'].get(dev_id)
|
||||
entity = hass.data[OT_DOMAIN]['devices'].get(dev_id)
|
||||
|
||||
if device is not None:
|
||||
device.update_data(
|
||||
host_name=host_name,
|
||||
gps=gps,
|
||||
attributes=attributes,
|
||||
gps_accuracy=gps_accuracy,
|
||||
battery=battery,
|
||||
source_type=source_type,
|
||||
location_name=location_name,
|
||||
)
|
||||
if entity is not None:
|
||||
entity.update_data(data)
|
||||
return
|
||||
|
||||
device = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
|
||||
dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=gps,
|
||||
attributes=attributes,
|
||||
gps_accuracy=gps_accuracy,
|
||||
battery=battery,
|
||||
source_type=source_type,
|
||||
location_name=location_name,
|
||||
entity = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
|
||||
dev_id, data
|
||||
)
|
||||
async_add_entities([device])
|
||||
async_add_entities([entity])
|
||||
|
||||
hass.data[OT_DOMAIN]['context'].set_async_see(_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] == OT_DOMAIN
|
||||
}
|
||||
|
||||
if not dev_ids:
|
||||
return True
|
||||
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
entity = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
|
||||
dev_id
|
||||
)
|
||||
entities.append(entity)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
hass.data[OT_DOMAIN]['context'].async_see = _receive_data
|
||||
return True
|
||||
|
||||
|
||||
class OwnTracksEntity(DeviceTrackerEntity):
|
||||
class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
def __init__(self, dev_id, host_name, gps, attributes, gps_accuracy,
|
||||
battery, source_type, location_name):
|
||||
def __init__(self, dev_id, data=None):
|
||||
"""Set up OwnTracks entity."""
|
||||
self._dev_id = dev_id
|
||||
self._host_name = host_name
|
||||
self._gps = gps
|
||||
self._gps_accuracy = gps_accuracy
|
||||
self._location_name = location_name
|
||||
self._attributes = attributes
|
||||
self._battery = battery
|
||||
self._source_type = source_type
|
||||
self._data = data or {}
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
||||
@property
|
||||
@@ -71,43 +79,45 @@ class OwnTracksEntity(DeviceTrackerEntity):
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the battery level of the device."""
|
||||
return self._battery
|
||||
return self._data.get('battery')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific attributes."""
|
||||
return self._attributes
|
||||
return self._data.get('attributes')
|
||||
|
||||
@property
|
||||
def location_accuracy(self):
|
||||
"""Return the gps accuracy of the device."""
|
||||
return self._gps_accuracy
|
||||
return self._data.get('gps_accuracy')
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
"""Return latitude value of the device."""
|
||||
if self._gps is not None:
|
||||
return self._gps[0]
|
||||
# Check with "get" instead of "in" because value can be None
|
||||
if self._data.get('gps'):
|
||||
return self._data['gps'][0]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
"""Return longitude value of the device."""
|
||||
if self._gps is not None:
|
||||
return self._gps[1]
|
||||
# Check with "get" instead of "in" because value can be None
|
||||
if self._data.get('gps'):
|
||||
return self._data['gps'][1]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def location_name(self):
|
||||
"""Return a location name for the current location of the device."""
|
||||
return self._location_name
|
||||
return self._data.get('location_name')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._host_name
|
||||
return self._data.get('host_name')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -117,26 +127,40 @@ class OwnTracksEntity(DeviceTrackerEntity):
|
||||
@property
|
||||
def source_type(self):
|
||||
"""Return the source type, eg gps or router, of the device."""
|
||||
return self._source_type
|
||||
return self._data.get('source_type', SOURCE_TYPE_GPS)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return {
|
||||
'name': self._host_name,
|
||||
'name': self.name,
|
||||
'identifiers': {(OT_DOMAIN, self._dev_id)},
|
||||
}
|
||||
|
||||
@callback
|
||||
def update_data(self, host_name, gps, attributes, gps_accuracy,
|
||||
battery, source_type, location_name):
|
||||
"""Mark the device as seen."""
|
||||
self._host_name = host_name
|
||||
self._gps = gps
|
||||
self._gps_accuracy = gps_accuracy
|
||||
self._location_name = location_name
|
||||
self._attributes = attributes
|
||||
self._battery = battery
|
||||
self._source_type = source_type
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Don't restore if we got set up with data.
|
||||
if self._data:
|
||||
return
|
||||
|
||||
state = await self.async_get_last_state()
|
||||
|
||||
if state is None:
|
||||
return
|
||||
|
||||
attr = state.attributes
|
||||
self._data = {
|
||||
'host_name': state.name,
|
||||
'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
|
||||
def update_data(self, data):
|
||||
"""Mark the device as seen."""
|
||||
self._data = data
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -163,6 +170,7 @@ class Sun(Entity):
|
||||
utc_point_in_time, 'dusk', PHASE_ASTRONOMICAL_TWILIGHT)
|
||||
self.next_midnight = self._check_event(
|
||||
utc_point_in_time, 'solar_midnight', None)
|
||||
self.location.solar_depression = 'civil'
|
||||
|
||||
# if the event was solar midday or midnight, phase will now
|
||||
# be None. Solar noon doesn't always happen when the sun is
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,28 +6,43 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant import config_entries
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .config_flow import async_get_devices
|
||||
from .const import DOMAIN
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .common import (
|
||||
async_discover_devices,
|
||||
get_static_devices,
|
||||
ATTR_CONFIG,
|
||||
CONF_DIMMER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_LIGHT,
|
||||
CONF_SWITCH,
|
||||
SmartDevices
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'tplink'
|
||||
|
||||
TPLINK_HOST_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
})
|
||||
|
||||
CONF_LIGHT = 'light'
|
||||
CONF_SWITCH = 'switch'
|
||||
CONF_DISCOVERY = 'discovery'
|
||||
|
||||
ATTR_CONFIG = 'config'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional('light', default=[]): vol.All(cv.ensure_list,
|
||||
[TPLINK_HOST_SCHEMA]),
|
||||
vol.Optional('switch', default=[]): vol.All(cv.ensure_list,
|
||||
[TPLINK_HOST_SCHEMA]),
|
||||
vol.Optional('discovery', default=True): cv.boolean,
|
||||
vol.Optional(CONF_LIGHT, default=[]): vol.All(
|
||||
cv.ensure_list,
|
||||
[TPLINK_HOST_SCHEMA]
|
||||
),
|
||||
vol.Optional(CONF_SWITCH, default=[]): vol.All(
|
||||
cv.ensure_list,
|
||||
[TPLINK_HOST_SCHEMA]
|
||||
),
|
||||
vol.Optional(CONF_DIMMER, default=[]): vol.All(
|
||||
cv.ensure_list,
|
||||
[TPLINK_HOST_SCHEMA]
|
||||
),
|
||||
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@@ -46,76 +61,45 @@ async def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigType):
|
||||
"""Set up TPLink from a config entry."""
|
||||
from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException
|
||||
|
||||
devices = {}
|
||||
|
||||
config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
|
||||
|
||||
# These will contain the initialized devices
|
||||
lights = hass.data[DOMAIN][CONF_LIGHT] = []
|
||||
switches = hass.data[DOMAIN][CONF_SWITCH] = []
|
||||
|
||||
# If discovery is defined and not disabled, discover devices
|
||||
# If initialized from configure integrations, there's no config
|
||||
# so we default here to True
|
||||
if config_data is None or config_data[CONF_DISCOVERY]:
|
||||
devs = await async_get_devices(hass)
|
||||
_LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs))
|
||||
devices.update(devs)
|
||||
|
||||
def _device_for_type(host, type_):
|
||||
dev = None
|
||||
if type_ == CONF_LIGHT:
|
||||
dev = SmartBulb(host)
|
||||
elif type_ == CONF_SWITCH:
|
||||
dev = SmartPlug(host)
|
||||
|
||||
return dev
|
||||
|
||||
# When arriving from configure integrations, we have no config data.
|
||||
# Add static devices
|
||||
static_devices = SmartDevices()
|
||||
if config_data is not None:
|
||||
for type_ in [CONF_LIGHT, CONF_SWITCH]:
|
||||
for entry in config_data[type_]:
|
||||
try:
|
||||
host = entry['host']
|
||||
dev = _device_for_type(host, type_)
|
||||
devices[host] = dev
|
||||
_LOGGER.debug("Succesfully added %s %s: %s",
|
||||
type_, host, dev)
|
||||
except SmartDeviceException as ex:
|
||||
_LOGGER.error("Unable to initialize %s %s: %s",
|
||||
type_, host, ex)
|
||||
static_devices = get_static_devices(
|
||||
config_data,
|
||||
)
|
||||
|
||||
# This is necessary to avoid I/O blocking on is_dimmable
|
||||
def _fill_device_lists():
|
||||
for dev in devices.values():
|
||||
if isinstance(dev, SmartPlug):
|
||||
try:
|
||||
if dev.is_dimmable: # Dimmers act as lights
|
||||
lights.append(dev)
|
||||
else:
|
||||
switches.append(dev)
|
||||
except SmartDeviceException as ex:
|
||||
_LOGGER.error("Unable to connect to device %s: %s",
|
||||
dev.host, ex)
|
||||
lights.extend(static_devices.lights)
|
||||
switches.extend(static_devices.switches)
|
||||
|
||||
elif isinstance(dev, SmartBulb):
|
||||
lights.append(dev)
|
||||
else:
|
||||
_LOGGER.error("Unknown smart device type: %s", type(dev))
|
||||
# Add discovered devices
|
||||
if config_data is None or config_data[CONF_DISCOVERY]:
|
||||
discovered_devices = await async_discover_devices(hass, static_devices)
|
||||
|
||||
# Avoid blocking on is_dimmable
|
||||
await hass.async_add_executor_job(_fill_device_lists)
|
||||
lights.extend(discovered_devices.lights)
|
||||
switches.extend(discovered_devices.switches)
|
||||
|
||||
forward_setup = hass.config_entries.async_forward_entry_setup
|
||||
if lights:
|
||||
_LOGGER.debug("Got %s lights: %s", len(lights), lights)
|
||||
_LOGGER.debug(
|
||||
"Got %s lights: %s",
|
||||
len(lights),
|
||||
", ".join([d.host for d in lights])
|
||||
)
|
||||
hass.async_create_task(forward_setup(config_entry, 'light'))
|
||||
if switches:
|
||||
_LOGGER.debug("Got %s switches: %s", len(switches), switches)
|
||||
_LOGGER.debug(
|
||||
"Got %s switches: %s",
|
||||
len(switches),
|
||||
", ".join([d.host for d in switches])
|
||||
)
|
||||
hass.async_create_task(forward_setup(config_entry, 'switch'))
|
||||
|
||||
return True
|
||||
|
||||
202
homeassistant/components/tplink/common.py
Normal file
202
homeassistant/components/tplink/common.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""Common code for tplink."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Callable, List
|
||||
|
||||
from pyHS100 import (
|
||||
SmartBulb,
|
||||
SmartDevice,
|
||||
SmartPlug,
|
||||
SmartDeviceException
|
||||
)
|
||||
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ATTR_CONFIG = 'config'
|
||||
CONF_DIMMER = 'dimmer'
|
||||
CONF_DISCOVERY = 'discovery'
|
||||
CONF_LIGHT = 'light'
|
||||
CONF_SWITCH = 'switch'
|
||||
|
||||
|
||||
class SmartDevices:
|
||||
"""Hold different kinds of devices."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lights: List[SmartDevice] = None,
|
||||
switches: List[SmartDevice] = None
|
||||
):
|
||||
"""Constructor."""
|
||||
self._lights = lights or []
|
||||
self._switches = switches or []
|
||||
|
||||
@property
|
||||
def lights(self):
|
||||
"""Get the lights."""
|
||||
return self._lights
|
||||
|
||||
@property
|
||||
def switches(self):
|
||||
"""Get the switches."""
|
||||
return self._switches
|
||||
|
||||
def has_device_with_host(self, host):
|
||||
"""Check if a devices exists with a specific host."""
|
||||
for device in self.lights + self.switches:
|
||||
if device.host == host:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def async_get_discoverable_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pyHS100 import Discover
|
||||
|
||||
def discover():
|
||||
devs = Discover.discover()
|
||||
return devs
|
||||
return await hass.async_add_executor_job(discover)
|
||||
|
||||
|
||||
async def async_discover_devices(
|
||||
hass: HomeAssistantType,
|
||||
existing_devices: SmartDevices
|
||||
) -> SmartDevices:
|
||||
"""Get devices through discovery."""
|
||||
_LOGGER.debug("Discovering devices")
|
||||
devices = await async_get_discoverable_devices(hass)
|
||||
_LOGGER.info(
|
||||
"Discovered %s TP-Link smart home device(s)",
|
||||
len(devices)
|
||||
)
|
||||
|
||||
lights = []
|
||||
switches = []
|
||||
|
||||
def process_devices():
|
||||
for dev in devices.values():
|
||||
# If this device already exists, ignore dynamic setup.
|
||||
if existing_devices.has_device_with_host(dev.host):
|
||||
continue
|
||||
|
||||
if isinstance(dev, SmartPlug):
|
||||
try:
|
||||
if dev.is_dimmable: # Dimmers act as lights
|
||||
lights.append(dev)
|
||||
else:
|
||||
switches.append(dev)
|
||||
except SmartDeviceException as ex:
|
||||
_LOGGER.error("Unable to connect to device %s: %s",
|
||||
dev.host, ex)
|
||||
|
||||
elif isinstance(dev, SmartBulb):
|
||||
lights.append(dev)
|
||||
else:
|
||||
_LOGGER.error("Unknown smart device type: %s", type(dev))
|
||||
|
||||
await hass.async_add_executor_job(process_devices)
|
||||
|
||||
return SmartDevices(lights, switches)
|
||||
|
||||
|
||||
def get_static_devices(config_data) -> SmartDevices:
|
||||
"""Get statically defined devices in the config."""
|
||||
_LOGGER.debug("Getting static devices")
|
||||
lights = []
|
||||
switches = []
|
||||
|
||||
for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]:
|
||||
for entry in config_data[type_]:
|
||||
host = entry['host']
|
||||
|
||||
if type_ == CONF_LIGHT:
|
||||
lights.append(SmartBulb(host))
|
||||
elif type_ == CONF_SWITCH:
|
||||
switches.append(SmartPlug(host))
|
||||
# Dimmers need to be defined as smart plugs to work correctly.
|
||||
elif type_ == CONF_DIMMER:
|
||||
lights.append(SmartPlug(host))
|
||||
|
||||
return SmartDevices(
|
||||
lights,
|
||||
switches
|
||||
)
|
||||
|
||||
|
||||
async def async_add_entities_retry(
|
||||
hass: HomeAssistantType,
|
||||
async_add_entities: Callable[[List[Any], bool], None],
|
||||
objects: List[Any],
|
||||
callback: Callable[[Any, Callable], None],
|
||||
interval: timedelta = timedelta(seconds=60)
|
||||
):
|
||||
"""
|
||||
Add entities now and retry later if issues are encountered.
|
||||
|
||||
If the callback throws an exception or returns false, that
|
||||
object will try again a while later.
|
||||
This is useful for devices that are not online when hass starts.
|
||||
:param hass:
|
||||
:param async_add_entities: The callback provided to a
|
||||
platform's async_setup.
|
||||
:param objects: The objects to create as entities.
|
||||
:param callback: The callback that will perform the add.
|
||||
:param interval: THe time between attempts to add.
|
||||
:return: A callback to cancel the retries.
|
||||
"""
|
||||
add_objects = objects.copy()
|
||||
|
||||
is_cancelled = False
|
||||
|
||||
def cancel_interval_callback():
|
||||
nonlocal is_cancelled
|
||||
is_cancelled = True
|
||||
|
||||
async def process_objects_loop(delay: int):
|
||||
if is_cancelled:
|
||||
return
|
||||
|
||||
await process_objects()
|
||||
|
||||
if not add_objects:
|
||||
return
|
||||
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
hass.async_create_task(process_objects_loop(delay))
|
||||
|
||||
async def process_objects(*args):
|
||||
# Process each object.
|
||||
for add_object in list(add_objects):
|
||||
# Call the individual item callback.
|
||||
try:
|
||||
_LOGGER.debug(
|
||||
"Attempting to add object of type %s",
|
||||
type(add_object)
|
||||
)
|
||||
result = await hass.async_add_job(
|
||||
callback,
|
||||
add_object,
|
||||
async_add_entities
|
||||
)
|
||||
except SmartDeviceException as ex:
|
||||
_LOGGER.debug(
|
||||
str(ex)
|
||||
)
|
||||
result = False
|
||||
|
||||
if result is True or result is None:
|
||||
_LOGGER.debug("Added object.")
|
||||
add_objects.remove(add_object)
|
||||
else:
|
||||
_LOGGER.debug("Failed to add object, will try again later")
|
||||
|
||||
await process_objects_loop(interval.seconds)
|
||||
|
||||
return cancel_interval_callback
|
||||
@@ -2,19 +2,10 @@
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
from pyHS100 import Discover
|
||||
|
||||
def discover():
|
||||
devs = Discover.discover()
|
||||
return devs
|
||||
return await hass.async_add_executor_job(discover)
|
||||
from .common import async_get_discoverable_devices
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(DOMAIN,
|
||||
'TP-Link Smart Home',
|
||||
async_get_devices,
|
||||
async_get_discoverable_devices,
|
||||
config_entries.CONN_CLASS_LOCAL_POLL)
|
||||
|
||||
@@ -41,6 +41,12 @@ def get_scanner(hass, config):
|
||||
should be gradually migrated in the pypi package
|
||||
|
||||
"""
|
||||
_LOGGER.warning("TP-Link device tracker is unmaintained and will be "
|
||||
"removed in the future releases if no maintainer is "
|
||||
"found. If you have interest in this integration, "
|
||||
"feel free to create a pull request to move this code "
|
||||
"to a new 'tplink_router' integration and refactoring "
|
||||
"the device-specific parts to the tplink library")
|
||||
for cls in [
|
||||
TplinkDeviceScanner, Tplink5DeviceScanner, Tplink4DeviceScanner,
|
||||
Tplink3DeviceScanner, Tplink2DeviceScanner, Tplink1DeviceScanner
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pyHS100 import SmartBulb, SmartDeviceException
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_kelvin_to_mired as kelvin_to_mired,
|
||||
color_temperature_mired_to_kelvin as mired_to_kelvin)
|
||||
|
||||
from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN
|
||||
from .common import async_add_entities_retry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -31,17 +35,35 @@ async def async_setup_platform(hass, config, add_entities,
|
||||
'convert to use the tplink component.')
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up discovered switches."""
|
||||
devs = []
|
||||
for dev in hass.data[TPLINK_DOMAIN][CONF_LIGHT]:
|
||||
devs.append(TPLinkSmartBulb(dev))
|
||||
|
||||
async_add_entities(devs, True)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
config_entry,
|
||||
async_add_entities
|
||||
):
|
||||
"""Set up switches."""
|
||||
await async_add_entities_retry(
|
||||
hass,
|
||||
async_add_entities,
|
||||
hass.data[TPLINK_DOMAIN][CONF_LIGHT],
|
||||
add_entity
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def add_entity(device: SmartBulb, async_add_entities):
|
||||
"""Check if device is online and add the entity."""
|
||||
# Attempt to get the sysinfo. If it fails, it will raise an
|
||||
# exception that is caught by async_add_entities_retry which
|
||||
# will try again later.
|
||||
device.get_sysinfo()
|
||||
|
||||
async_add_entities(
|
||||
[TPLinkSmartBulb(device)],
|
||||
update_before_add=True
|
||||
)
|
||||
|
||||
|
||||
def brightness_to_percentage(byt):
|
||||
"""Convert brightness from absolute 0..255 to percentage."""
|
||||
return int((byt*100.0)/255.0)
|
||||
@@ -55,7 +77,7 @@ def brightness_from_percentage(percent):
|
||||
class TPLinkSmartBulb(Light):
|
||||
"""Representation of a TPLink Smart Bulb."""
|
||||
|
||||
def __init__(self, smartbulb) -> None:
|
||||
def __init__(self, smartbulb: SmartBulb) -> None:
|
||||
"""Initialize the bulb."""
|
||||
self.smartbulb = smartbulb
|
||||
self._sysinfo = None
|
||||
@@ -69,25 +91,29 @@ class TPLinkSmartBulb(Light):
|
||||
self._max_mireds = None
|
||||
self._emeter_params = {}
|
||||
|
||||
self._mac = None
|
||||
self._alias = None
|
||||
self._model = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._sysinfo["mac"]
|
||||
return self._mac
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Smart Bulb."""
|
||||
return self._sysinfo["alias"]
|
||||
return self._alias
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return information about the device."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"model": self._sysinfo["model"],
|
||||
"name": self._alias,
|
||||
"model": self._model,
|
||||
"manufacturer": 'TP-Link',
|
||||
"connections": {
|
||||
(dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"])
|
||||
(dr.CONNECTION_NETWORK_MAC, self._mac)
|
||||
},
|
||||
"sw_version": self._sysinfo["sw_ver"],
|
||||
}
|
||||
@@ -104,7 +130,6 @@ class TPLinkSmartBulb(Light):
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
from pyHS100 import SmartBulb
|
||||
self.smartbulb.state = SmartBulb.BULB_STATE_ON
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
@@ -122,7 +147,6 @@ class TPLinkSmartBulb(Light):
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
from pyHS100 import SmartBulb
|
||||
self.smartbulb.state = SmartBulb.BULB_STATE_OFF
|
||||
|
||||
@property
|
||||
@@ -157,7 +181,6 @@ class TPLinkSmartBulb(Light):
|
||||
|
||||
def update(self):
|
||||
"""Update the TP-Link Bulb's state."""
|
||||
from pyHS100 import SmartDeviceException, SmartBulb
|
||||
try:
|
||||
if self._supported_features is None:
|
||||
self.get_features()
|
||||
@@ -212,6 +235,9 @@ class TPLinkSmartBulb(Light):
|
||||
"""Determine all supported features in one go."""
|
||||
self._sysinfo = self.smartbulb.sys_info
|
||||
self._supported_features = 0
|
||||
self._mac = self.smartbulb.mac
|
||||
self._alias = self.smartbulb.alias
|
||||
self._model = self.smartbulb.model
|
||||
|
||||
if self.smartbulb.is_dimmable:
|
||||
self._supported_features += SUPPORT_BRIGHTNESS
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pyHS100 import SmartDeviceException, SmartPlug
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, SwitchDevice)
|
||||
from homeassistant.const import ATTR_VOLTAGE
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN
|
||||
from .common import async_add_entities_retry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -27,13 +31,31 @@ async def async_setup_platform(hass, config, add_entities,
|
||||
'convert to use the tplink component.')
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up discovered switches."""
|
||||
devs = []
|
||||
for dev in hass.data[TPLINK_DOMAIN][CONF_SWITCH]:
|
||||
devs.append(SmartPlugSwitch(dev))
|
||||
def add_entity(device: SmartPlug, async_add_entities):
|
||||
"""Check if device is online and add the entity."""
|
||||
# Attempt to get the sysinfo. If it fails, it will raise an
|
||||
# exception that is caught by async_add_entities_retry which
|
||||
# will try again later.
|
||||
device.get_sysinfo()
|
||||
|
||||
async_add_entities(devs, True)
|
||||
async_add_entities(
|
||||
[SmartPlugSwitch(device)],
|
||||
update_before_add=True
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
config_entry,
|
||||
async_add_entities
|
||||
):
|
||||
"""Set up switches."""
|
||||
await async_add_entities_retry(
|
||||
hass,
|
||||
async_add_entities,
|
||||
hass.data[TPLINK_DOMAIN][CONF_SWITCH],
|
||||
add_entity
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -41,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class SmartPlugSwitch(SwitchDevice):
|
||||
"""Representation of a TPLink Smart Plug switch."""
|
||||
|
||||
def __init__(self, smartplug):
|
||||
def __init__(self, smartplug: SmartPlug):
|
||||
"""Initialize the switch."""
|
||||
self.smartplug = smartplug
|
||||
self._sysinfo = None
|
||||
@@ -50,25 +72,29 @@ class SmartPlugSwitch(SwitchDevice):
|
||||
# Set up emeter cache
|
||||
self._emeter_params = {}
|
||||
|
||||
self._mac = None
|
||||
self._alias = None
|
||||
self._model = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._sysinfo["mac"]
|
||||
return self._mac
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Smart Plug."""
|
||||
return self._sysinfo["alias"]
|
||||
return self._alias
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return information about the device."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"model": self._sysinfo["model"],
|
||||
"name": self._alias,
|
||||
"model": self._model,
|
||||
"manufacturer": 'TP-Link',
|
||||
"connections": {
|
||||
(dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"])
|
||||
(dr.CONNECTION_NETWORK_MAC, self._mac)
|
||||
},
|
||||
"sw_version": self._sysinfo["sw_ver"],
|
||||
}
|
||||
@@ -98,10 +124,12 @@ class SmartPlugSwitch(SwitchDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the TP-Link switch's state."""
|
||||
from pyHS100 import SmartDeviceException
|
||||
try:
|
||||
if not self._sysinfo:
|
||||
self._sysinfo = self.smartplug.sys_info
|
||||
self._mac = self.smartplug.mac
|
||||
self._alias = self.smartplug.alias
|
||||
self._model = self.smartplug.model
|
||||
|
||||
self._state = self.smartplug.state == \
|
||||
self.smartplug.SWITCH_STATE_ON
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -23,7 +23,7 @@ SUPPORTED_VOICES = [
|
||||
"de-DE_BirgitVoice",
|
||||
"de-DE_BirgitV2Voice",
|
||||
"de-DE_DieterVoice",
|
||||
"de-DE_DieterV2Voice"
|
||||
"de-DE_DieterV2Voice",
|
||||
"en-GB_KateVoice",
|
||||
"en-US_AllisonVoice",
|
||||
"en-US_AllisonV2Voice",
|
||||
|
||||
@@ -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
|
||||
|
||||
15
homeassistant/components/wemo/config_flow.py
Normal file
15
homeassistant/components/wemo/config_flow.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Config flow for Wemo."""
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
from homeassistant import config_entries
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
async def _async_has_devices(hass):
|
||||
"""Return if there are devices that can be discovered."""
|
||||
import pywemo
|
||||
|
||||
return bool(pywemo.discover_devices())
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, 'Wemo', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH)
|
||||
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"domain": "wemo",
|
||||
"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"
|
||||
|
||||
15
homeassistant/components/wemo/strings.json
Normal file
15
homeassistant/components/wemo/strings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "Wemo",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "Wemo",
|
||||
"description": "Do you want to set up Wemo?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of Wemo is possible.",
|
||||
"no_devices_found": "No Wemo devices found on the network."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ from homeassistant.util import convert
|
||||
from homeassistant.const import (
|
||||
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."""
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 94
|
||||
PATCH_VERSION = '0b2'
|
||||
PATCH_VERSION = '4'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -49,6 +49,7 @@ FLOWS = [
|
||||
"twilio",
|
||||
"unifi",
|
||||
"upnp",
|
||||
"wemo",
|
||||
"zha",
|
||||
"zone",
|
||||
"zwave"
|
||||
|
||||
@@ -7,7 +7,11 @@ To update, run python3 -m hassfest
|
||||
SSDP = {
|
||||
"device_type": {},
|
||||
"manufacturer": {
|
||||
"Belkin International Inc.": [
|
||||
"wemo"
|
||||
],
|
||||
"Royal Philips Electronics": [
|
||||
"deconz",
|
||||
"hue"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -20,5 +20,8 @@ ZEROCONF = {
|
||||
}
|
||||
|
||||
HOMEKIT = {
|
||||
"LIFX ": "lifx"
|
||||
"BSB002": "hue",
|
||||
"LIFX": "lifx",
|
||||
"TRADFRI": "tradfri",
|
||||
"Wemo": "wemo"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,14 +44,27 @@ 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()
|
||||
kwargs = {
|
||||
'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE)
|
||||
'constraints': os.path.join(os.path.dirname(__file__),
|
||||
CONSTRAINT_FILE),
|
||||
'no_cache_dir': is_docker,
|
||||
}
|
||||
if 'WHEELS_LINKS' in os.environ:
|
||||
kwargs['find_links'] = os.environ['WHEELS_LINKS']
|
||||
if not (config_dir is None or pkg_util.is_virtual_env()) and \
|
||||
not pkg_util.is_docker_env():
|
||||
not is_docker:
|
||||
kwargs['target'] = os.path.join(config_dir, 'deps')
|
||||
return kwargs
|
||||
|
||||
@@ -49,7 +49,8 @@ def is_installed(package: str) -> bool:
|
||||
def install_package(package: str, upgrade: bool = True,
|
||||
target: Optional[str] = None,
|
||||
constraints: Optional[str] = None,
|
||||
find_links: Optional[str] = None) -> bool:
|
||||
find_links: Optional[str] = None,
|
||||
no_cache_dir: Optional[bool] = False) -> bool:
|
||||
"""Install a package on PyPi. Accepts pip compatible package strings.
|
||||
|
||||
Return boolean if install successful.
|
||||
@@ -58,6 +59,8 @@ def install_package(package: str, upgrade: bool = True,
|
||||
_LOGGER.info('Attempting install of %s', package)
|
||||
env = os.environ.copy()
|
||||
args = [sys.executable, '-m', 'pip', 'install', '--quiet', package]
|
||||
if no_cache_dir:
|
||||
args.append('--no-cache-dir')
|
||||
if upgrade:
|
||||
args.append('--upgrade')
|
||||
if constraints is not None:
|
||||
|
||||
@@ -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
|
||||
@@ -547,7 +547,7 @@ habitipy==0.2.0
|
||||
hangups==0.4.9
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.13
|
||||
hass-nabucasa==0.14
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
hbmqtt==0.9.4
|
||||
@@ -577,7 +577,7 @@ hole==0.3.0
|
||||
holidays==0.9.10
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20190530.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
|
||||
|
||||
@@ -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
|
||||
@@ -136,7 +136,7 @@ ha-ffmpeg==2.0
|
||||
hangups==0.4.9
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.13
|
||||
hass-nabucasa==0.14
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
hbmqtt==0.9.4
|
||||
@@ -148,7 +148,7 @@ hdate==0.8.7
|
||||
holidays==0.9.10
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20190530.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -281,3 +281,30 @@ async def test_discovery_already_configured_name(hass, mock_client):
|
||||
result = await flow.async_step_zeroconf(user_input=service_info)
|
||||
assert result['type'] == 'abort'
|
||||
assert result['reason'] == 'already_configured'
|
||||
|
||||
|
||||
async def test_discovery_duplicate_data(hass, mock_client):
|
||||
"""Test discovery aborts if same mDNS packet arrives."""
|
||||
service_info = {
|
||||
'host': '192.168.43.183',
|
||||
'port': 6053,
|
||||
'hostname': 'test8266.local.',
|
||||
'properties': {
|
||||
"address": "test8266.local"
|
||||
}
|
||||
}
|
||||
|
||||
mock_client.device_info.return_value = mock_coro(
|
||||
MockDeviceInfo(False, "test8266"))
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
'esphome', data=service_info, context={'source': 'zeroconf'}
|
||||
)
|
||||
assert result['type'] == 'form'
|
||||
assert result['step_id'] == 'discovery_confirm'
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
'esphome', data=service_info, context={'source': 'zeroconf'}
|
||||
)
|
||||
assert result['type'] == 'abort'
|
||||
assert result['reason'] == 'already_configured'
|
||||
|
||||
@@ -5,13 +5,12 @@ from unittest.mock import patch, Mock
|
||||
import pytest
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components import zone, geofency
|
||||
from homeassistant.components import zone
|
||||
from homeassistant.components.geofency import (
|
||||
CONF_MOBILE_BEACONS, DOMAIN, TRACKER_UPDATE)
|
||||
CONF_MOBILE_BEACONS, DOMAIN)
|
||||
from homeassistant.const import (
|
||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME,
|
||||
STATE_NOT_HOME)
|
||||
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import slugify
|
||||
|
||||
@@ -217,6 +216,12 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id):
|
||||
'device_tracker', device_name)).attributes['longitude']
|
||||
assert NOT_HOME_LONGITUDE == current_longitude
|
||||
|
||||
dev_reg = await hass.helpers.device_registry.async_get_registry()
|
||||
assert len(dev_reg.devices) == 1
|
||||
|
||||
ent_reg = await hass.helpers.entity_registry.async_get_registry()
|
||||
assert len(ent_reg.entities) == 1
|
||||
|
||||
|
||||
async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id):
|
||||
"""Test iBeacon based zone enter and exit - a.k.a stationary iBeacon."""
|
||||
@@ -285,9 +290,6 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
|
||||
assert STATE_HOME == state_name
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason='The device_tracker component does not support unloading yet.'
|
||||
)
|
||||
async def test_load_unload_entry(hass, geofency_client, webhook_id):
|
||||
"""Test that the appropriate dispatch signals are added and removed."""
|
||||
url = '/api/webhook/{}'.format(webhook_id)
|
||||
@@ -297,13 +299,23 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id):
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
device_name = slugify(GPS_ENTER_HOME['device'])
|
||||
state_name = hass.states.get('{}.{}'.format(
|
||||
'device_tracker', device_name)).state
|
||||
assert STATE_HOME == state_name
|
||||
assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1
|
||||
state_1 = hass.states.get('{}.{}'.format('device_tracker', device_name))
|
||||
assert STATE_HOME == state_1.state
|
||||
|
||||
assert len(hass.data[DOMAIN]['devices']) == 1
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
assert await geofency.async_unload_entry(hass, entry)
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE]
|
||||
assert len(hass.data[DOMAIN]['devices']) == 0
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state_2 = hass.states.get('{}.{}'.format('device_tracker', device_name))
|
||||
assert state_2 is not None
|
||||
assert state_1 is not state_2
|
||||
|
||||
assert STATE_HOME == state_2.state
|
||||
assert state_2.attributes['latitude'] == HOME_LATITUDE
|
||||
assert state_2.attributes['longitude'] == HOME_LONGITUDE
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -140,6 +140,12 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id):
|
||||
data['device'])).state
|
||||
assert STATE_NOT_HOME == state_name
|
||||
|
||||
dev_reg = await hass.helpers.device_registry.async_get_registry()
|
||||
assert len(dev_reg.devices) == 1
|
||||
|
||||
ent_reg = await hass.helpers.entity_registry.async_get_registry()
|
||||
assert len(ent_reg.entities) == 1
|
||||
|
||||
|
||||
async def test_enter_with_attrs(hass, gpslogger_client, webhook_id):
|
||||
"""Test when additional attributes are present."""
|
||||
@@ -172,6 +178,33 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id):
|
||||
assert state.attributes['provider'] == 'gps'
|
||||
assert state.attributes['activity'] == 'running'
|
||||
|
||||
data = {
|
||||
'latitude': HOME_LATITUDE,
|
||||
'longitude': HOME_LONGITUDE,
|
||||
'device': '123',
|
||||
'accuracy': 123,
|
||||
'battery': 23,
|
||||
'speed': 23,
|
||||
'direction': 123,
|
||||
'altitude': 123,
|
||||
'provider': 'gps',
|
||||
'activity': 'idle'
|
||||
}
|
||||
|
||||
req = await gpslogger_client.post(url, data=data)
|
||||
await hass.async_block_till_done()
|
||||
assert req.status == HTTP_OK
|
||||
state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
|
||||
data['device']))
|
||||
assert state.state == STATE_HOME
|
||||
assert state.attributes['gps_accuracy'] == 123
|
||||
assert state.attributes['battery_level'] == 23
|
||||
assert state.attributes['speed'] == 23
|
||||
assert state.attributes['direction'] == 123
|
||||
assert state.attributes['altitude'] == 123
|
||||
assert state.attributes['provider'] == 'gps'
|
||||
assert state.attributes['activity'] == 'idle'
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason='The device_tracker component does not support unloading yet.'
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,74 +1 @@
|
||||
"""Tests for mobile_app component."""
|
||||
# pylint: disable=redefined-outer-name,unused-import
|
||||
import pytest
|
||||
|
||||
from tests.common import mock_device_registry
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from homeassistant.components.mobile_app.const import (DATA_BINARY_SENSOR,
|
||||
DATA_DELETED_IDS,
|
||||
DATA_SENSOR,
|
||||
DOMAIN,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION)
|
||||
|
||||
from .const import REGISTER, REGISTER_CLEARTEXT
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry(hass):
|
||||
"""Return a configured device registry."""
|
||||
return mock_device_registry(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def create_registrations(authed_api_client):
|
||||
"""Return two new registrations."""
|
||||
enc_reg = await authed_api_client.post(
|
||||
'/api/mobile_app/registrations', json=REGISTER
|
||||
)
|
||||
|
||||
assert enc_reg.status == 201
|
||||
enc_reg_json = await enc_reg.json()
|
||||
|
||||
clear_reg = await authed_api_client.post(
|
||||
'/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
|
||||
)
|
||||
|
||||
assert clear_reg.status == 201
|
||||
clear_reg_json = await clear_reg.json()
|
||||
|
||||
return (enc_reg_json, clear_reg_json)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user):
|
||||
"""mobile_app mock client."""
|
||||
hass_storage[STORAGE_KEY] = {
|
||||
'version': STORAGE_VERSION,
|
||||
'data': {
|
||||
DATA_BINARY_SENSOR: {},
|
||||
DATA_DELETED_IDS: [],
|
||||
DATA_SENSOR: {}
|
||||
}
|
||||
}
|
||||
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
return await aiohttp_client(hass.http.app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def authed_api_client(hass, hass_client):
|
||||
"""Provide an authenticated client for mobile_app to use."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
return await hass_client()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_ws(hass):
|
||||
"""Configure the websocket_api component."""
|
||||
assert await async_setup_component(hass, 'websocket_api', {})
|
||||
await hass.async_block_till_done()
|
||||
"""Tests for the mobile app integration."""
|
||||
|
||||
60
tests/components/mobile_app/conftest.py
Normal file
60
tests/components/mobile_app/conftest.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for mobile_app component."""
|
||||
# pylint: disable=redefined-outer-name,unused-import
|
||||
import pytest
|
||||
|
||||
from tests.common import mock_device_registry
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from homeassistant.components.mobile_app.const import DOMAIN
|
||||
|
||||
from .const import REGISTER, REGISTER_CLEARTEXT
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry(hass):
|
||||
"""Return a configured device registry."""
|
||||
return mock_device_registry(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def create_registrations(authed_api_client):
|
||||
"""Return two new registrations."""
|
||||
enc_reg = await authed_api_client.post(
|
||||
'/api/mobile_app/registrations', json=REGISTER
|
||||
)
|
||||
|
||||
assert enc_reg.status == 201
|
||||
enc_reg_json = await enc_reg.json()
|
||||
|
||||
clear_reg = await authed_api_client.post(
|
||||
'/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
|
||||
)
|
||||
|
||||
assert clear_reg.status == 201
|
||||
clear_reg_json = await clear_reg.json()
|
||||
|
||||
return (enc_reg_json, clear_reg_json)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def webhook_client(hass, aiohttp_client):
|
||||
"""mobile_app mock client."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
return await aiohttp_client(hass.http.app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def authed_api_client(hass, hass_client):
|
||||
"""Provide an authenticated client for mobile_app to use."""
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
return await hass_client()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_ws(hass):
|
||||
"""Configure the websocket_api component."""
|
||||
assert await async_setup_component(hass, 'websocket_api', {})
|
||||
await hass.async_block_till_done()
|
||||
116
tests/components/mobile_app/test_device_tracker.py
Normal file
116
tests/components/mobile_app/test_device_tracker.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Test mobile app device tracker."""
|
||||
|
||||
|
||||
async def test_sending_location(hass, create_registrations, webhook_client):
|
||||
"""Test sending a location via a webhook."""
|
||||
resp = await webhook_client.post(
|
||||
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||
json={
|
||||
'type': 'update_location',
|
||||
'data': {
|
||||
'gps': [10, 20],
|
||||
'gps_accuracy': 30,
|
||||
'battery': 40,
|
||||
'altitude': 50,
|
||||
'course': 60,
|
||||
'speed': 70,
|
||||
'vertical_accuracy': 80,
|
||||
'location_name': 'bar',
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get('device_tracker.test_1_2')
|
||||
assert state is not None
|
||||
assert state.name == 'Test 1'
|
||||
assert state.state == 'bar'
|
||||
assert state.attributes['source_type'] == 'gps'
|
||||
assert state.attributes['latitude'] == 10
|
||||
assert state.attributes['longitude'] == 20
|
||||
assert state.attributes['gps_accuracy'] == 30
|
||||
assert state.attributes['battery_level'] == 40
|
||||
assert state.attributes['altitude'] == 50
|
||||
assert state.attributes['course'] == 60
|
||||
assert state.attributes['speed'] == 70
|
||||
assert state.attributes['vertical_accuracy'] == 80
|
||||
|
||||
resp = await webhook_client.post(
|
||||
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||
json={
|
||||
'type': 'update_location',
|
||||
'data': {
|
||||
'gps': [1, 2],
|
||||
'gps_accuracy': 3,
|
||||
'battery': 4,
|
||||
'altitude': 5,
|
||||
'course': 6,
|
||||
'speed': 7,
|
||||
'vertical_accuracy': 8,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get('device_tracker.test_1_2')
|
||||
assert state is not None
|
||||
assert state.state == 'not_home'
|
||||
assert state.attributes['source_type'] == 'gps'
|
||||
assert state.attributes['latitude'] == 1
|
||||
assert state.attributes['longitude'] == 2
|
||||
assert state.attributes['gps_accuracy'] == 3
|
||||
assert state.attributes['battery_level'] == 4
|
||||
assert state.attributes['altitude'] == 5
|
||||
assert state.attributes['course'] == 6
|
||||
assert state.attributes['speed'] == 7
|
||||
assert state.attributes['vertical_accuracy'] == 8
|
||||
|
||||
|
||||
async def test_restoring_location(hass, create_registrations, webhook_client):
|
||||
"""Test sending a location via a webhook."""
|
||||
resp = await webhook_client.post(
|
||||
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||
json={
|
||||
'type': 'update_location',
|
||||
'data': {
|
||||
'gps': [10, 20],
|
||||
'gps_accuracy': 30,
|
||||
'battery': 40,
|
||||
'altitude': 50,
|
||||
'course': 60,
|
||||
'speed': 70,
|
||||
'vertical_accuracy': 80,
|
||||
'location_name': 'bar',
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assert resp.status == 200
|
||||
await hass.async_block_till_done()
|
||||
state_1 = hass.states.get('device_tracker.test_1_2')
|
||||
assert state_1 is not None
|
||||
|
||||
config_entry = hass.config_entries.async_entries('mobile_app')[1]
|
||||
|
||||
# mobile app doesn't support unloading, so we just reload device tracker
|
||||
await hass.config_entries.async_forward_entry_unload(config_entry,
|
||||
'device_tracker')
|
||||
await hass.config_entries.async_forward_entry_setup(config_entry,
|
||||
'device_tracker')
|
||||
|
||||
state_2 = hass.states.get('device_tracker.test_1_2')
|
||||
assert state_2 is not None
|
||||
|
||||
assert state_1 is not state_2
|
||||
assert state_2.name == 'Test 1'
|
||||
assert state_2.attributes['source_type'] == 'gps'
|
||||
assert state_2.attributes['latitude'] == 10
|
||||
assert state_2.attributes['longitude'] == 20
|
||||
assert state_2.attributes['gps_accuracy'] == 30
|
||||
assert state_2.attributes['battery_level'] == 40
|
||||
assert state_2.attributes['altitude'] == 50
|
||||
assert state_2.attributes['course'] == 60
|
||||
assert state_2.attributes['speed'] == 70
|
||||
assert state_2.attributes['vertical_accuracy'] == 80
|
||||
@@ -2,9 +2,6 @@
|
||||
# pylint: disable=redefined-outer-name,unused-import
|
||||
import logging
|
||||
|
||||
from . import (authed_api_client, create_registrations, # noqa: F401
|
||||
webhook_client) # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -7,10 +7,9 @@ from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import REGISTER, RENDER_TEMPLATE
|
||||
from . import authed_api_client # noqa: F401
|
||||
|
||||
|
||||
async def test_registration(hass, hass_client): # noqa: F811
|
||||
async def test_registration(hass, hass_client):
|
||||
"""Test that registrations happen."""
|
||||
try:
|
||||
# pylint: disable=unused-import
|
||||
|
||||
@@ -11,17 +11,14 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_mock_service
|
||||
|
||||
from . import (authed_api_client, create_registrations, # noqa: F401
|
||||
webhook_client) # noqa: F401
|
||||
|
||||
from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT,
|
||||
RENDER_TEMPLATE, UPDATE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def test_webhook_handle_render_template(create_registrations, # noqa: F401, F811, E501
|
||||
webhook_client): # noqa: F811
|
||||
async def test_webhook_handle_render_template(create_registrations,
|
||||
webhook_client):
|
||||
"""Test that we render templates properly."""
|
||||
resp = await webhook_client.post(
|
||||
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||
@@ -34,7 +31,7 @@ async def test_webhook_handle_render_template(create_registrations, # noqa: F40
|
||||
assert json == {'one': 'Hello world'}
|
||||
|
||||
|
||||
async def test_webhook_handle_call_services(hass, create_registrations, # noqa: F401, F811, E501
|
||||
async def test_webhook_handle_call_services(hass, create_registrations,
|
||||
webhook_client): # noqa: E501 F811
|
||||
"""Test that we call services properly."""
|
||||
calls = async_mock_service(hass, 'test', 'mobile_app')
|
||||
@@ -49,8 +46,8 @@ async def test_webhook_handle_call_services(hass, create_registrations, # noqa:
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F401, F811, E501
|
||||
webhook_client): # noqa: F811
|
||||
async def test_webhook_handle_fire_event(hass, create_registrations,
|
||||
webhook_client):
|
||||
"""Test that we can fire events."""
|
||||
events = []
|
||||
|
||||
@@ -76,7 +73,7 @@ async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F4
|
||||
|
||||
async def test_webhook_update_registration(webhook_client, hass_client): # noqa: E501 F811
|
||||
"""Test that a we can update an existing registration via webhook."""
|
||||
authed_api_client = await hass_client() # noqa: F811
|
||||
authed_api_client = await hass_client()
|
||||
register_resp = await authed_api_client.post(
|
||||
'/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
|
||||
)
|
||||
@@ -102,8 +99,8 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa
|
||||
assert CONF_SECRET not in update_json
|
||||
|
||||
|
||||
async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F401, F811, E501
|
||||
webhook_client): # noqa: F811
|
||||
async def test_webhook_handle_get_zones(hass, create_registrations,
|
||||
webhook_client):
|
||||
"""Test that we can get zones properly."""
|
||||
await async_setup_component(hass, ZONE_DOMAIN, {
|
||||
ZONE_DOMAIN: {
|
||||
@@ -126,8 +123,8 @@ async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F40
|
||||
assert json[0]['entity_id'] == 'zone.home'
|
||||
|
||||
|
||||
async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F401, F811, E501
|
||||
webhook_client): # noqa: F811
|
||||
async def test_webhook_handle_get_config(hass, create_registrations,
|
||||
webhook_client):
|
||||
"""Test that we can get config properly."""
|
||||
resp = await webhook_client.post(
|
||||
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
|
||||
@@ -160,8 +157,8 @@ async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F4
|
||||
assert expected_dict == json
|
||||
|
||||
|
||||
async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501
|
||||
create_registrations, # noqa: F401, F811, E501
|
||||
async def test_webhook_returns_error_incorrect_json(webhook_client,
|
||||
create_registrations,
|
||||
caplog): # noqa: E501 F811
|
||||
"""Test that an error is returned when JSON is invalid."""
|
||||
resp = await webhook_client.post(
|
||||
@@ -175,8 +172,8 @@ async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F40
|
||||
assert 'invalid JSON' in caplog.text
|
||||
|
||||
|
||||
async def test_webhook_handle_decryption(webhook_client, # noqa: F811
|
||||
create_registrations): # noqa: F401, F811, E501
|
||||
async def test_webhook_handle_decryption(webhook_client,
|
||||
create_registrations):
|
||||
"""Test that we can encrypt/decrypt properly."""
|
||||
try:
|
||||
# pylint: disable=unused-import
|
||||
@@ -221,8 +218,8 @@ async def test_webhook_handle_decryption(webhook_client, # noqa: F811
|
||||
assert json.loads(decrypted_data) == {'one': 'Hello world'}
|
||||
|
||||
|
||||
async def test_webhook_requires_encryption(webhook_client, # noqa: F811
|
||||
create_registrations): # noqa: F401, F811, E501
|
||||
async def test_webhook_requires_encryption(webhook_client,
|
||||
create_registrations):
|
||||
"""Test that encrypted registrations only accept encrypted data."""
|
||||
resp = await webhook_client.post(
|
||||
'/api/webhook/{}'.format(create_registrations[0]['webhook_id']),
|
||||
|
||||
@@ -5,7 +5,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import authed_api_client, setup_ws, webhook_client # noqa: F401
|
||||
from .const import (CALL_SERVICE, REGISTER)
|
||||
|
||||
|
||||
@@ -45,7 +44,7 @@ async def test_webocket_get_user_registrations(hass, aiohttp_client,
|
||||
|
||||
|
||||
async def test_webocket_delete_registration(hass, hass_client,
|
||||
hass_ws_client, webhook_client): # noqa: E501 F811
|
||||
hass_ws_client, webhook_client):
|
||||
"""Test delete_registration websocket command."""
|
||||
authed_api_client = await hass_client() # noqa: F811
|
||||
register_resp = await authed_api_client.post(
|
||||
|
||||
@@ -411,6 +411,19 @@ async def test_location_update(hass, context):
|
||||
"""Test the update of a location."""
|
||||
await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE)
|
||||
|
||||
assert_location_source_type(hass, 'gps')
|
||||
assert_location_latitude(hass, LOCATION_MESSAGE['lat'])
|
||||
assert_location_accuracy(hass, LOCATION_MESSAGE['acc'])
|
||||
assert_location_state(hass, 'outer')
|
||||
|
||||
|
||||
async def test_location_update_no_t_key(hass, context):
|
||||
"""Test the update of a location when message does not contain 't'."""
|
||||
message = LOCATION_MESSAGE.copy()
|
||||
message.pop('t')
|
||||
await send_message(hass, LOCATION_TOPIC, message)
|
||||
|
||||
assert_location_source_type(hass, 'gps')
|
||||
assert_location_latitude(hass, LOCATION_MESSAGE['lat'])
|
||||
assert_location_accuracy(hass, LOCATION_MESSAGE['acc'])
|
||||
assert_location_state(hass, 'outer')
|
||||
@@ -1491,3 +1504,47 @@ async def test_region_mapping(hass, setup_comp):
|
||||
|
||||
await send_message(hass, EVENT_TOPIC, message)
|
||||
assert_location_state(hass, 'inner')
|
||||
|
||||
|
||||
async def test_restore_state(hass, hass_client):
|
||||
"""Test that we can restore state."""
|
||||
entry = MockConfigEntry(domain='owntracks', data={
|
||||
'webhook_id': 'owntracks_test',
|
||||
'secret': 'abcd',
|
||||
})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_client()
|
||||
resp = await client.post(
|
||||
'/api/webhook/owntracks_test',
|
||||
json=LOCATION_MESSAGE,
|
||||
headers={
|
||||
'X-Limit-u': 'Paulus',
|
||||
'X-Limit-d': 'Pixel',
|
||||
}
|
||||
)
|
||||
assert resp.status == 200
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state_1 = hass.states.get('device_tracker.paulus_pixel')
|
||||
assert state_1 is not None
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state_2 = hass.states.get('device_tracker.paulus_pixel')
|
||||
assert state_2 is not None
|
||||
|
||||
assert state_1 is not state_2
|
||||
|
||||
assert state_1.state == state_2.state
|
||||
assert state_1.name == state_2.name
|
||||
assert state_1.attributes['latitude'] == state_2.attributes['latitude']
|
||||
assert state_1.attributes['longitude'] == state_2.attributes['longitude']
|
||||
assert state_1.attributes['battery_level'] == \
|
||||
state_2.attributes['battery_level']
|
||||
assert state_1.attributes['source_type'] == \
|
||||
state_2.attributes['source_type']
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user