Compare commits

..

54 Commits

Author SHA1 Message Date
Paulus Schoutsen
ac4d5d7c30 Merge pull request #21778 from home-assistant/rc
0.89.1
2019-03-07 23:16:17 -08:00
Paulus Schoutsen
f3e8e34089 Add workflow for tests 2019-03-07 17:03:23 -08:00
Paulus Schoutsen
eae6d1c7a6 Bumped version to 0.89.1 2019-03-07 16:48:53 -08:00
William Scanlon
a121c92f52 Updated to newest pyeconet (#21772) 2019-03-07 16:48:47 -08:00
David Thulke
4d6f21ecb2 adds missing SUPPORT_VOLUME_SET flag to webos media_player (#21766) 2019-03-07 16:48:46 -08:00
Sebastian Muszynski
1638d0a92f Bump PyXiaomiGateway version to 0.12.2 (Closes: #21731) (#21764) 2019-03-07 16:48:46 -08:00
Jason Hu
c031fd4164 Fix script load module issue (#21763)
* Fix script load depedency

* Revert #21754
2019-03-07 16:48:45 -08:00
Jason Hu
5f0c37ccfc Fix colorlog import error (#21754)
* Fix colorlog import error

* Lint
2019-03-07 11:07:24 -08:00
Daniel Shokouhi
e412317194 Fix botvac connected maps call as it is not a supported model (#21752) 2019-03-07 11:06:42 -08:00
Leonardo Merza
44341a958a automated commit 07/03/2019 10:47:38 (#21749) 2019-03-07 11:06:41 -08:00
Markus Jankowski
5a555102b9 Fix group-switch availability for Homematic IP (#21640)
* Add available=True to groups

* Added unreach to stateattributes

* Fixed comments

* added missing sabotage check

* added missing lowBat check

* fix typo

* apply suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* apply suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* applied suggiestions

* readded lost str()

* fix comment
2019-03-07 11:06:40 -08:00
Markus Jankowski
aebe6ab70c Fix Name of Homematic IP accesspoint in devices, if name is configured (#21617)
* Fix Name of Accesspoint if name is configured

* fix lint

* Simplyfied naming

* applied suggestion

Co-Authored-By: SukramJ <markus@mm-jankowski.de>

* update comment
2019-03-07 11:06:39 -08:00
Kevin Fronczak
48c9758cf5 Upgrade blinkpy==0.13.1 (Fixes #21559) (#21578)
* Upgrade blinkpy with new api endpoint

* Change wifi units to dBm
2019-03-07 11:06:39 -08:00
Paulus Schoutsen
279470613c Updated frontend to 20190305.1 2019-03-07 10:54:56 -08:00
Paulus Schoutsen
88bc3033d3 Merge pull request #21712 from home-assistant/rc
0.89.0
2019-03-06 15:15:37 -08:00
Paulus Schoutsen
21de636e5b Bumped version to 0.89.0 2019-03-06 10:07:31 -08:00
Franck Nijhof
87b5faa244 Upgrade toonapilib to 3.2.1 (#21706) 2019-03-06 10:05:32 -08:00
Fredrik Erlandsson
c2f4293c6a resync hass that changes have occured (#21705) 2019-03-06 10:05:31 -08:00
Paulus Schoutsen
4c72f3c48b Bumped version to 0.89.0b3 2019-03-05 11:46:30 -08:00
carstenschroeder
cb613984df Fix ADS race condition (#21677) 2019-03-05 11:46:19 -08:00
Diogo Gomes
4978a1681e check we have a tb (#21670) 2019-03-05 11:46:18 -08:00
Paulus Schoutsen
2303e1684e Updated frontend to 20190305.0 2019-03-05 11:45:46 -08:00
Paulus Schoutsen
3135257c0d Bumped version to 0.89.0b2 2019-03-04 16:02:05 -08:00
Paulus Schoutsen
b20b811cb9 Avoid recorder thread crashing (#21668) 2019-03-04 16:01:59 -08:00
Franck Nijhof
a778cd117f Upgrade toonapilib to 3.1.0 (#21661) 2019-03-04 16:01:15 -08:00
Franck Nijhof
31b88197eb 🚑 Fixes Toon doing I/O in coroutines (#21657) 2019-03-04 16:01:14 -08:00
Paulus Schoutsen
81c252f917 Rename Google Assistant evenets (#21655) 2019-03-04 16:01:13 -08:00
Franck Nijhof
f5a0b5ab98 👕 Corrects unit of measurement symbol for Watt (#21654) 2019-03-04 16:01:12 -08:00
Gijs Reichert
a382ba731d Cast displaytime to int for JSON RPC (#21649) 2019-03-04 15:59:19 -08:00
Paulus Schoutsen
cca8d4c951 Fix calc next (#21630) 2019-03-04 15:59:18 -08:00
Anders Melchiorsen
932080656d Upgrade pysonos to 0.0.8 (#21624) 2019-03-04 15:59:18 -08:00
Jason Hu
d5bdfdb0b3 Resolve race condition when HA auth provider is loading (#21619)
* Resolve race condition when HA auth provider is loading

* Fix

* Add more tests

* Lint
2019-03-04 15:59:17 -08:00
Andrew Sayre
d9806f759b Handle when installed app has already been removed (#21595) 2019-03-04 15:59:16 -08:00
Anders Melchiorsen
e6debe09e8 Word the tplink deprecation warning more strongly (#21586) 2019-03-04 15:59:16 -08:00
Jason Hu
c5dad82211 Log exception occurred in WS service call command (#21584) 2019-03-04 15:59:15 -08:00
Daniel Høyer Iversen
ec9ccf6402 Upgrade PyXiaomiGateway library (#21582) 2019-03-04 15:59:15 -08:00
Jason Hu
a268aab2ec Re-thrown exception occurred in the blocking service call (#21573)
* Rethrown exception occurred in the actual service call

* Fix lint and test
2019-03-04 15:59:14 -08:00
damarco
996e0a6389 Bump zigpy-deconz (#21566) 2019-03-04 15:59:14 -08:00
emontnemery
e877983533 Make time trigger data trigger.now local (#21544)
* Make time trigger data trigger.now local

* Make time pattern trigger data trigger.now local

* Lint

* Rework according to review comment

* Lint
2019-03-04 15:59:13 -08:00
Robbie Trencheny
73675d5a48 mobile_app component (#21475)
* Initial pass of a mobile_app component

* Fully support encryption, validation for the webhook payloads, and other general improvements

* Return same format as original API calls

* Minor encryption fixes, logging improvements

* Migrate Owntracks to use the superior PyNaCl instead of libnacl, mark it as a requirement in mobile_app

* Add mobile_app to .coveragerc

* Dont manually b64decode on OT

* Initial requested changes

* Round two of fixes

* Initial mobile_app tests

* Dont allow making registration requests for same/existing device

* Test formatting fixes

* Add mobile_app to default_config

* Add some more keys allowed in registration payloads

* Add support for getting a single device, updating a device, getting all devices. Also change from /api/mobile_app/register to /api/mobile_app/devices

* Change device_id to fingerprint

* Next round of changes

* Add keyword args and pass context on all relevant calls

* Remove SingleDeviceView in favor of webhook type to update registration

* Only allow some properties to be updated on registrations, rename integration_data to app_data

* Add call service test, ensure events actually fire, only run the encryption tests if sodium is installed

* pylint

* Fix OwnTracks test

* Fix iteration of devices and remove device_for_webhook_id
2019-03-04 15:59:12 -08:00
Paulus Schoutsen
43f85f7053 Updated frontend to 20190303.0 2019-03-03 22:41:38 -08:00
Paulus Schoutsen
ed28482311 Bumped version to 0.89.0b1 2019-02-28 17:58:23 -08:00
Aaron Bach
0f09c02875 Fix incorrect pyairvisual call (#21542) 2019-02-28 17:46:32 -08:00
Jason Hu
97b93bcf7b Fix warning (#21538) 2019-02-28 17:46:31 -08:00
emontnemery
b05062e9d9 Add missing retain option to mqtt.climate configuration schema (#21536) 2019-02-28 17:46:31 -08:00
Victor Vostrikov
6f2dd21516 Updated variable name for readability (#21528) 2019-02-28 17:46:30 -08:00
Jason Hu
eda2290d47 Allow skip-pip applied to HA core (#21527) 2019-02-28 17:46:30 -08:00
Paulus Schoutsen
238c4247d9 Only use a single store instance (#21521) 2019-02-28 17:46:30 -08:00
Paulus Schoutsen
4fe9f966ad Fix lint (#21520) 2019-02-28 17:46:29 -08:00
Anders Melchiorsen
26a534a67c Improve new Sonos snapshot/restore (#21509)
* Fine-tune new Sonos snapshot/restore

* Move into class
2019-02-28 17:46:29 -08:00
Aaron Bach
aa546b5a1f Add watchdog to Ambient PWS (#21507)
* Add watchdog to Ambient PWS

* Better labeling

* Owner comments
2019-02-28 17:46:28 -08:00
Robert Svensson
9e140d27bf Fix deCONZ retry mechanism for setup 2019-02-28 17:45:41 -08:00
Ben Randall
e6cbdf0645 Add PLATFORM_SCHEMA_BASE to telegram_bot component (#21155) 2019-02-28 17:44:04 -08:00
Paulus Schoutsen
1c889cfcc3 Updated frontend to 20190228.0 2019-02-28 17:43:48 -08:00
68 changed files with 1215 additions and 223 deletions

View File

@@ -320,6 +320,7 @@ omit =
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/ziggo_mediabox_xl.py
homeassistant/components/meteo_france/*
homeassistant/components/mobile_app/*
homeassistant/components/mochad/*
homeassistant/components/modbus/*
homeassistant/components/mychevy/*
@@ -384,7 +385,7 @@ omit =
homeassistant/components/point/*
homeassistant/components/prometheus/*
homeassistant/components/ps4/__init__.py
homeassistant/components/ps4/media_player.py
homeassistant/components/ps4/media_player.py
homeassistant/components/qwikswitch/*
homeassistant/components/rachio/*
homeassistant/components/rainbird/*

41
.github/main.workflow vendored Normal file
View File

@@ -0,0 +1,41 @@
workflow "Python 3.7 - tox" {
resolves = ["Python 3.7 - tests"]
on = "push"
}
action "Python 3.7 - tests" {
uses = "home-assistant/actions/py37-tox@master"
args = "-e py37"
}
workflow "Python 3.6 - tox" {
resolves = ["Python 3.6 - tests"]
on = "push"
}
action "Python 3.6 - tests" {
uses = "home-assistant/actions/py36-tox@master"
args = "-e py36"
}
workflow "Python 3.5 - tox" {
resolves = ["Pyton 3.5 - typing"]
on = "push"
}
action "Python 3.5 - tests" {
uses = "home-assistant/actions/py35-tox@master"
args = "-e py35"
}
action "Python 3.5 - lints" {
uses = "home-assistant/actions/py35-tox@master"
needs = ["Python 3.5 - tests"]
args = "-e lint"
}
action "Pyton 3.5 - typing" {
uses = "home-assistant/actions/py35-tox@master"
args = "-e typing"
needs = ["Python 3.5 - lints"]
}

View File

@@ -2,6 +2,7 @@
Sending HOTP through notify service
"""
import asyncio
import logging
from collections import OrderedDict
from typing import Any, Dict, Optional, List
@@ -90,6 +91,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
self._include = config.get(CONF_INCLUDE, [])
self._exclude = config.get(CONF_EXCLUDE, [])
self._message_template = config[CONF_MESSAGE]
self._init_lock = asyncio.Lock()
@property
def input_schema(self) -> vol.Schema:
@@ -98,15 +100,19 @@ class NotifyAuthModule(MultiFactorAuthModule):
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
async with self._init_lock:
if self._user_settings is not None:
return
if data is None:
data = {STORAGE_USERS: {}}
data = await self._user_store.async_load()
self._user_settings = {
user_id: NotifySetting(**setting)
for user_id, setting in data.get(STORAGE_USERS, {}).items()
}
if data is None:
data = {STORAGE_USERS: {}}
self._user_settings = {
user_id: NotifySetting(**setting)
for user_id, setting in data.get(STORAGE_USERS, {}).items()
}
async def _async_save(self) -> None:
"""Save data."""

View File

@@ -1,4 +1,5 @@
"""Time-based One Time Password auth module."""
import asyncio
import logging
from io import BytesIO
from typing import Any, Dict, Optional, Tuple # noqa: F401
@@ -68,6 +69,7 @@ class TotpAuthModule(MultiFactorAuthModule):
self._users = None # type: Optional[Dict[str, str]]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True)
self._init_lock = asyncio.Lock()
@property
def input_schema(self) -> vol.Schema:
@@ -76,12 +78,16 @@ class TotpAuthModule(MultiFactorAuthModule):
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
async with self._init_lock:
if self._users is not None:
return
if data is None:
data = {STORAGE_USERS: {}}
data = await self._user_store.async_load()
self._users = data.get(STORAGE_USERS, {})
if data is None:
data = {STORAGE_USERS: {}}
self._users = data.get(STORAGE_USERS, {})
async def _async_save(self) -> None:
"""Save data."""

View File

@@ -1,4 +1,5 @@
"""Home Assistant auth provider."""
import asyncio
import base64
from collections import OrderedDict
import logging
@@ -204,15 +205,21 @@ class HassAuthProvider(AuthProvider):
DEFAULT_TITLE = 'Home Assistant Local'
data = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize an Home Assistant auth provider."""
super().__init__(*args, **kwargs)
self.data = None # type: Optional[Data]
self._init_lock = asyncio.Lock()
async def async_initialize(self) -> None:
"""Initialize the auth provider."""
if self.data is not None:
return
async with self._init_lock:
if self.data is not None:
return
self.data = Data(self.hass)
await self.data.async_load()
data = Data(self.hass)
await data.async_load()
self.data = data
async def async_login_flow(
self, context: Optional[Dict]) -> LoginFlow:

View File

@@ -85,6 +85,11 @@ async def async_from_config_dict(config: Dict[str, Any],
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
hass.config.skip_pip = skip_pip
if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. "
"This may cause issues")
core_config = config.get(core.DOMAIN, {})
has_api_password = bool(config.get('http', {}).get('api_password'))
trusted_networks = config.get('http', {}).get('trusted_networks')
@@ -104,11 +109,6 @@ async def async_from_config_dict(config: Dict[str, Any],
await hass.async_add_executor_job(
conf_util.process_ha_config_upgrade, hass)
hass.config.skip_pip = skip_pip
if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. "
"This may cause issues")
# Make a copy because we are mutating it.
config = OrderedDict(config)

View File

@@ -171,13 +171,12 @@ class AdsHub:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback)
hnotify = int(hnotify)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback)
_LOGGER.debug(
"Added device notification %d for variable %s", hnotify, name)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback)
def _device_notification_callback(self, notification, name):
"""Handle device notifications."""
contents = notification.contents
@@ -187,9 +186,10 @@ class AdsHub:
data = contents.data
try:
notification_item = self._notification_items[hnotify]
with self._lock:
notification_item = self._notification_items[hnotify]
except KeyError:
_LOGGER.debug("Unknown device notification handle: %d", hnotify)
_LOGGER.error("Unknown device notification handle: %d", hnotify)
return
# Parse data to desired datatype

View File

@@ -27,6 +27,7 @@ _LOGGER = logging.getLogger(__name__)
DATA_CONFIG = 'config'
DEFAULT_SOCKET_MIN_RETRY = 15
DEFAULT_WATCHDOG_SECONDS = 5 * 60
TYPE_24HOURRAININ = '24hourrainin'
TYPE_BAROMABSIN = 'baromabsin'
@@ -296,6 +297,7 @@ class AmbientStation:
"""Initialize."""
self._config_entry = config_entry
self._hass = hass
self._watchdog_listener = None
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self.client = client
self.monitored_conditions = monitored_conditions
@@ -305,9 +307,18 @@ class AmbientStation:
"""Register handlers and connect to the websocket."""
from aioambient.errors import WebsocketError
async def _ws_reconnect(event_time):
"""Forcibly disconnect from and reconnect to the websocket."""
_LOGGER.debug('Watchdog expired; forcing socket reconnection')
await self.client.websocket.disconnect()
await self.client.websocket.connect()
def on_connect():
"""Define a handler to fire when the websocket is connected."""
_LOGGER.info('Connected to websocket')
_LOGGER.debug('Watchdog starting')
self._watchdog_listener = async_call_later(
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect)
def on_data(data):
"""Define a handler to fire when the data is received."""
@@ -317,6 +328,11 @@ class AmbientStation:
self.stations[mac_address][ATTR_LAST_DATA] = data
async_dispatcher_send(self._hass, TOPIC_UPDATE)
_LOGGER.debug('Resetting watchdog')
self._watchdog_listener()
self._watchdog_listener = async_call_later(
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect)
def on_disconnect():
"""Define a handler to fire when the websocket is disconnected."""
_LOGGER.info('Disconnected from websocket')

View File

@@ -10,7 +10,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.12.1']
REQUIREMENTS = ['blinkpy==0.13.1']
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ BINARY_SENSORS = {
SENSORS = {
TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'],
TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'bars', 'mdi:wifi-strength-2'],
TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'],
}
BINARY_SENSOR_SCHEMA = vol.Schema({

View File

@@ -11,6 +11,7 @@ DEPENDENCIES = (
'history',
'logbook',
'map',
'mobile_app',
'person',
'script',
'sun',

View File

@@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass
from .storage import async_setup_frontend_storage
REQUIREMENTS = ['home-assistant-frontend==20190227.0']
REQUIREMENTS = ['home-assistant-frontend==20190305.1']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@@ -11,7 +11,7 @@ STORAGE_KEY_USER_DATA = 'frontend.user_data_{}'
async def async_setup_frontend_storage(hass):
"""Set up frontend storage."""
hass.data[DATA_STORAGE] = {}
hass.data[DATA_STORAGE] = ({}, {})
hass.components.websocket_api.async_register_command(
websocket_set_user_data
)
@@ -25,12 +25,16 @@ def with_store(orig_func):
@wraps(orig_func)
async def with_store_func(hass, connection, msg):
"""Provide user specific data and store to function."""
store = hass.helpers.storage.Store(
STORAGE_VERSION_USER_DATA,
STORAGE_KEY_USER_DATA.format(connection.user.id)
)
data = hass.data[DATA_STORAGE]
stores, data = hass.data[DATA_STORAGE]
user_id = connection.user.id
store = stores.get(user_id)
if store is None:
store = stores[user_id] = hass.helpers.storage.Store(
STORAGE_VERSION_USER_DATA,
STORAGE_KEY_USER_DATA.format(connection.user.id)
)
if user_id not in data:
data[user_id] = await store.async_load() or {}

View File

@@ -44,6 +44,6 @@ ERR_UNKNOWN_ERROR = 'unknownError'
ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported'
# Event types
EVENT_COMMAND_RECEIVED = 'google_assistant_command_received'
EVENT_QUERY_RECEIVED = 'google_assistant_query_received'
EVENT_SYNC_RECEIVED = 'google_assistant_sync_received'
EVENT_COMMAND_RECEIVED = 'google_assistant_command'
EVENT_QUERY_RECEIVED = 'google_assistant_query'
EVENT_SYNC_RECEIVED = 'google_assistant_sync'

View File

@@ -60,11 +60,14 @@ async def async_setup_entry(hass, entry):
# Register hap as device in registry.
device_registry = await dr.async_get_registry(hass)
home = hap.home
# Add the HAP name from configuration if set.
hapname = home.label \
if not home.name else "{} {}".format(home.label, home.name)
device_registry.async_get_or_create(
config_entry_id=home.id,
identifiers={(DOMAIN, home.id)},
manufacturer='eQ-3',
name=home.label,
name=hapname,
model=home.modelType,
sw_version=home.currentAPVersion,
)

View File

@@ -4,6 +4,8 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematicip_cloud import (
DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud.device import (
ATTR_GROUP_MEMBER_UNREACHABLE)
DEPENDENCIES = ['homematicip_cloud']
@@ -31,8 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
AsyncWaterSensor, AsyncRotaryHandleSensor,
AsyncMotionDetectorPushButton)
from homematicip.group import (
SecurityGroup, SecurityZoneGroup)
from homematicip.aio.group import (
AsyncSecurityGroup, AsyncSecurityZoneGroup)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
@@ -48,9 +50,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
devices.append(HomematicipWaterDetector(home, device))
for group in home.groups:
if isinstance(group, SecurityGroup):
if isinstance(group, AsyncSecurityGroup):
devices.append(HomematicipSecuritySensorGroup(home, group))
elif isinstance(group, SecurityZoneGroup):
elif isinstance(group, AsyncSecurityZoneGroup):
devices.append(HomematicipSecurityZoneSensorGroup(home, group))
if devices:
@@ -137,27 +139,37 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice,
"""Return the class of this sensor."""
return 'safety'
@property
def available(self):
"""Security-Group available."""
# A security-group must be available, and should not be affected by
# the individual availability of group members.
return True
@property
def device_state_attributes(self):
"""Return the state attributes of the security zone group."""
attr = super().device_state_attributes
if self._device.motionDetected:
attr.update({ATTR_MOTIONDETECTED: True})
attr[ATTR_MOTIONDETECTED] = True
if self._device.presenceDetected:
attr.update({ATTR_PRESENCEDETECTED: True})
attr[ATTR_PRESENCEDETECTED] = True
from homematicip.base.enums import WindowState
if self._device.windowState is not None and \
self._device.windowState != WindowState.CLOSED:
attr.update({ATTR_WINDOWSTATE: str(self._device.windowState)})
attr[ATTR_WINDOWSTATE] = str(self._device.windowState)
if self._device.unreach:
attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
return attr
@property
def is_on(self):
"""Return true if security issue detected."""
if self._device.motionDetected or \
self._device.presenceDetected:
self._device.presenceDetected or \
self._device.unreach or \
self._device.sabotage:
return True
from homematicip.base.enums import WindowState
if self._device.windowState is not None and \
@@ -180,29 +192,30 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup,
attr = super().device_state_attributes
if self._device.powerMainsFailure:
attr.update({ATTR_POWERMAINSFAILURE: True})
attr[ATTR_POWERMAINSFAILURE] = True
if self._device.moistureDetected:
attr.update({ATTR_MOISTUREDETECTED: True})
attr[ATTR_MOISTUREDETECTED] = True
if self._device.waterlevelDetected:
attr.update({ATTR_WATERLEVELDETECTED: True})
attr[ATTR_WATERLEVELDETECTED] = True
from homematicip.base.enums import SmokeDetectorAlarmType
if self._device.smokeDetectorAlarmType is not None and \
self._device.smokeDetectorAlarmType != \
SmokeDetectorAlarmType.IDLE_OFF:
attr.update({ATTR_SMOKEDETECTORALARM: str(
self._device.smokeDetectorAlarmType)})
attr[ATTR_SMOKEDETECTORALARM] = \
str(self._device.smokeDetectorAlarmType)
return attr
@property
def is_on(self):
"""Return true if security issue detected."""
"""Return true if safety issue detected."""
parent_is_on = super().is_on
from homematicip.base.enums import SmokeDetectorAlarmType
if parent_is_on or \
self._device.powerMainsFailure or \
self._device.moistureDetected or \
self._device.waterlevelDetected:
self._device.waterlevelDetected or \
self._device.lowBat:
return True
if self._device.smokeDetectorAlarmType is not None and \
self._device.smokeDetectorAlarmType != \

View File

@@ -21,6 +21,7 @@ ATTR_OPERATION_LOCK = 'operation_lock'
ATTR_SABOTAGE = 'sabotage'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_UNREACHABLE = 'unreachable'
ATTR_GROUP_MEMBER_UNREACHABLE = 'group_member_unreachable'
class HomematicipGenericDevice(Entity):

View File

@@ -3,6 +3,8 @@ import logging
from homeassistant.components.homematicip_cloud import (
DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice)
from homeassistant.components.homematicip_cloud.device import (
ATTR_GROUP_MEMBER_UNREACHABLE)
from homeassistant.components.switch import SwitchDevice
DEPENDENCIES = ['homematicip_cloud']
@@ -30,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
AsyncOpenCollector8Module,
)
from homematicip.group import SwitchingGroup
from homematicip.aio.group import AsyncSwitchingGroup
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
@@ -50,7 +52,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
devices.append(HomematicipMultiSwitch(home, device, channel))
for group in home.groups:
if isinstance(group, SwitchingGroup):
if isinstance(group, AsyncSwitchingGroup):
devices.append(
HomematicipGroupSwitch(home, group))
@@ -92,6 +94,23 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice):
"""Return true if group is on."""
return self._device.on
@property
def available(self):
"""Switch-Group available."""
# A switch-group must be available, and should not be affected by the
# individual availability of group members.
# This allows switching even when individual group members
# are not available.
return True
@property
def device_state_attributes(self):
"""Return the state attributes of the switch-group."""
attr = {}
if self._device.unreach:
attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True
return attr
async def async_turn_on(self, **kwargs):
"""Turn the group on."""
await self._device.turn_on()

View File

@@ -55,6 +55,9 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1
def trusted_networks_deprecated(value):
"""Warn user trusted_networks config is deprecated."""
if not value:
return value
_LOGGER.warning(
"Configuring trusted_networks via the http component has been"
" deprecated. Use the trusted networks auth provider instead."

View File

@@ -0,0 +1,355 @@
"""Support for native mobile apps."""
import logging
import json
from functools import partial
import voluptuous as vol
from aiohttp.web import json_response, Response
from aiohttp.web_exceptions import HTTPBadRequest
from homeassistant import config_entries
from homeassistant.auth.util import generate_secret
import homeassistant.core as ha
from homeassistant.core import Context
from homeassistant.components import webhook
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.device_tracker import (
DOMAIN as DEVICE_TRACKER_DOMAIN, SERVICE_SEE as DEVICE_TRACKER_SEE,
SERVICE_SEE_PAYLOAD_SCHEMA as SEE_SCHEMA)
from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA,
HTTP_BAD_REQUEST, HTTP_CREATED,
HTTP_INTERNAL_SERVER_ERROR, CONF_WEBHOOK_ID)
from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound,
TemplateError)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.typing import HomeAssistantType
REQUIREMENTS = ['PyNaCl==1.3.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'mobile_app'
DEPENDENCIES = ['device_tracker', 'http', 'webhook']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
CONF_SECRET = 'secret'
CONF_USER_ID = 'user_id'
ATTR_APP_DATA = 'app_data'
ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name'
ATTR_APP_VERSION = 'app_version'
ATTR_DEVICE_NAME = 'device_name'
ATTR_MANUFACTURER = 'manufacturer'
ATTR_MODEL = 'model'
ATTR_OS_VERSION = 'os_version'
ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption'
ATTR_EVENT_DATA = 'event_data'
ATTR_EVENT_TYPE = 'event_type'
ATTR_TEMPLATE = 'template'
ATTR_TEMPLATE_VARIABLES = 'variables'
ATTR_WEBHOOK_DATA = 'data'
ATTR_WEBHOOK_ENCRYPTED = 'encrypted'
ATTR_WEBHOOK_ENCRYPTED_DATA = 'encrypted_data'
ATTR_WEBHOOK_TYPE = 'type'
WEBHOOK_TYPE_CALL_SERVICE = 'call_service'
WEBHOOK_TYPE_FIRE_EVENT = 'fire_event'
WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template'
WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location'
WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration'
WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT,
WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION]
REGISTER_DEVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_APP_DATA, default={}): dict,
vol.Required(ATTR_APP_ID): cv.string,
vol.Optional(ATTR_APP_NAME): cv.string,
vol.Required(ATTR_APP_VERSION): cv.string,
vol.Required(ATTR_DEVICE_NAME): cv.string,
vol.Required(ATTR_MANUFACTURER): cv.string,
vol.Required(ATTR_MODEL): cv.string,
vol.Optional(ATTR_OS_VERSION): cv.string,
vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean,
})
UPDATE_DEVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_APP_DATA, default={}): dict,
vol.Required(ATTR_APP_VERSION): cv.string,
vol.Required(ATTR_DEVICE_NAME): cv.string,
vol.Required(ATTR_MANUFACTURER): cv.string,
vol.Required(ATTR_MODEL): cv.string,
vol.Optional(ATTR_OS_VERSION): cv.string,
})
WEBHOOK_PAYLOAD_SCHEMA = vol.Schema({
vol.Required(ATTR_WEBHOOK_TYPE): vol.In(WEBHOOK_TYPES),
vol.Required(ATTR_WEBHOOK_DATA, default={}): dict,
vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean,
vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string,
})
CALL_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_DOMAIN): cv.string,
vol.Required(ATTR_SERVICE): cv.string,
vol.Optional(ATTR_SERVICE_DATA, default={}): dict,
})
FIRE_EVENT_SCHEMA = vol.Schema({
vol.Required(ATTR_EVENT_TYPE): cv.string,
vol.Optional(ATTR_EVENT_DATA, default={}): dict,
})
RENDER_TEMPLATE_SCHEMA = vol.Schema({
vol.Required(ATTR_TEMPLATE): cv.string,
vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict,
})
WEBHOOK_SCHEMAS = {
WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA,
WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA,
WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA,
WEBHOOK_TYPE_UPDATE_LOCATION: SEE_SCHEMA,
WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_DEVICE_SCHEMA,
}
def get_cipher():
"""Return decryption function and length of key.
Async friendly.
"""
from nacl.secret import SecretBox
from nacl.encoding import Base64Encoder
def decrypt(ciphertext, key):
"""Decrypt ciphertext using key."""
return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder)
return (SecretBox.KEY_SIZE, decrypt)
def _decrypt_payload(key, ciphertext):
"""Decrypt encrypted payload."""
try:
keylen, decrypt = get_cipher()
except OSError:
_LOGGER.warning(
"Ignoring encrypted payload because libsodium not installed")
return None
if key is None:
_LOGGER.warning(
"Ignoring encrypted payload because no decryption key known")
return None
key = key.encode("utf-8")
key = key[:keylen]
key = key.ljust(keylen, b'\0')
try:
message = decrypt(ciphertext, key)
message = json.loads(message.decode("utf-8"))
_LOGGER.debug("Successfully decrypted mobile_app payload")
return message
except ValueError:
_LOGGER.warning("Ignoring encrypted payload because unable to decrypt")
return None
def context(device):
"""Generate a context from a request."""
return Context(user_id=device[CONF_USER_ID])
async def handle_webhook(store, hass: HomeAssistantType, webhook_id: str,
request):
"""Handle webhook callback."""
device = hass.data[DOMAIN][webhook_id]
try:
req_data = await request.json()
except ValueError:
_LOGGER.warning('Received invalid JSON from mobile_app')
return json_response([], status=HTTP_BAD_REQUEST)
try:
req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data)
except vol.Invalid as ex:
err = vol.humanize.humanize_error(req_data, ex)
_LOGGER.error('Received invalid webhook payload: %s', err)
return Response(status=200)
webhook_type = req_data[ATTR_WEBHOOK_TYPE]
webhook_payload = req_data.get(ATTR_WEBHOOK_DATA, {})
if req_data[ATTR_WEBHOOK_ENCRYPTED]:
enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA]
webhook_payload = _decrypt_payload(device[CONF_SECRET], enc_data)
try:
data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload)
except vol.Invalid as ex:
err = vol.humanize.humanize_error(webhook_payload, ex)
_LOGGER.error('Received invalid webhook payload: %s', err)
return Response(status=200)
if webhook_type == WEBHOOK_TYPE_CALL_SERVICE:
try:
await hass.services.async_call(data[ATTR_DOMAIN],
data[ATTR_SERVICE],
data[ATTR_SERVICE_DATA],
blocking=True,
context=context(device))
except (vol.Invalid, ServiceNotFound):
raise HTTPBadRequest()
return Response(status=200)
if webhook_type == WEBHOOK_TYPE_FIRE_EVENT:
event_type = data[ATTR_EVENT_TYPE]
hass.bus.async_fire(event_type, data[ATTR_EVENT_DATA],
ha.EventOrigin.remote, context=context(device))
return Response(status=200)
if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE:
try:
tpl = template.Template(data[ATTR_TEMPLATE], hass)
rendered = tpl.async_render(data.get(ATTR_TEMPLATE_VARIABLES))
return json_response({"rendered": rendered})
except (ValueError, TemplateError) as ex:
return json_response(({"error": ex}), status=HTTP_BAD_REQUEST)
if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION:
await hass.services.async_call(DEVICE_TRACKER_DOMAIN,
DEVICE_TRACKER_SEE, data,
blocking=True, context=context(device))
return Response(status=200)
if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION:
data[ATTR_APP_ID] = device[ATTR_APP_ID]
data[ATTR_APP_NAME] = device[ATTR_APP_NAME]
data[ATTR_SUPPORTS_ENCRYPTION] = device[ATTR_SUPPORTS_ENCRYPTION]
data[CONF_SECRET] = device[CONF_SECRET]
data[CONF_USER_ID] = device[CONF_USER_ID]
data[CONF_WEBHOOK_ID] = device[CONF_WEBHOOK_ID]
hass.data[DOMAIN][webhook_id] = data
try:
await store.async_save(hass.data[DOMAIN])
except HomeAssistantError as ex:
_LOGGER.error("Error updating mobile_app registration: %s", ex)
return Response(status=200)
return json_response(safe_device(data))
def supports_encryption():
"""Test if we support encryption."""
try:
import nacl # noqa pylint: disable=unused-import
return True
except OSError:
return False
def safe_device(device: dict):
"""Return a device without webhook_id or secret."""
return {
ATTR_APP_DATA: device[ATTR_APP_DATA],
ATTR_APP_ID: device[ATTR_APP_ID],
ATTR_APP_NAME: device[ATTR_APP_NAME],
ATTR_APP_VERSION: device[ATTR_APP_VERSION],
ATTR_DEVICE_NAME: device[ATTR_DEVICE_NAME],
ATTR_MANUFACTURER: device[ATTR_MANUFACTURER],
ATTR_MODEL: device[ATTR_MODEL],
ATTR_OS_VERSION: device[ATTR_OS_VERSION],
ATTR_SUPPORTS_ENCRYPTION: device[ATTR_SUPPORTS_ENCRYPTION],
}
def register_device_webhook(hass: HomeAssistantType, store, device):
"""Register the webhook for a device."""
device_name = 'Mobile App: {}'.format(device[ATTR_DEVICE_NAME])
webhook_id = device[CONF_WEBHOOK_ID]
webhook.async_register(hass, DOMAIN, device_name, webhook_id,
partial(handle_webhook, store))
async def async_setup(hass, config):
"""Set up the mobile app component."""
conf = config.get(DOMAIN)
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
app_config = await store.async_load()
if app_config is None:
app_config = {}
hass.data[DOMAIN] = app_config
for device in app_config.values():
register_device_webhook(hass, store, device)
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
hass.http.register_view(DevicesView(store))
return True
async def async_setup_entry(hass, entry):
"""Set up an mobile_app entry."""
return True
class DevicesView(HomeAssistantView):
"""A view that accepts device registration requests."""
url = '/api/mobile_app/devices'
name = 'api:mobile_app:register-device'
def __init__(self, store):
"""Initialize the view."""
self._store = store
@RequestDataValidator(REGISTER_DEVICE_SCHEMA)
async def post(self, request, data):
"""Handle the POST request for device registration."""
hass = request.app['hass']
resp = {}
webhook_id = generate_secret()
data[CONF_WEBHOOK_ID] = resp[CONF_WEBHOOK_ID] = webhook_id
if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption():
secret = generate_secret(16)
data[CONF_SECRET] = resp[CONF_SECRET] = secret
data[CONF_USER_ID] = request['hass_user'].id
hass.data[DOMAIN][webhook_id] = data
try:
await self._store.async_save(hass.data[DOMAIN])
except HomeAssistantError:
return self.json_message("Error saving device.",
HTTP_INTERNAL_SERVER_ERROR)
register_device_webhook(hass, self._store, data)
return self.json(resp, status_code=HTTP_CREATED)

View File

@@ -93,6 +93,7 @@ TEMPLATE_KEYS = (
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic,

View File

@@ -186,10 +186,11 @@ class NeatoConnectedVacuum(StateVacuumDevice):
self._battery_level = self._state['details']['charge']
if self._robot_has_map:
robot_map_id = self._robot_maps[self._robot_serial][0]['id']
if self._state['availableServices']['maps'] != "basic-1":
robot_map_id = self._robot_maps[self._robot_serial][0]['id']
self._robot_boundaries = self.robot.get_map_boundaries(
robot_map_id).json()
self._robot_boundaries = self.robot.get_map_boundaries(
robot_map_id).json()
@property
def name(self):

View File

@@ -90,7 +90,7 @@ class KodiNotificationService(BaseNotificationService):
try:
data = kwargs.get(ATTR_DATA) or {}
displaytime = data.get(ATTR_DISPLAYTIME, 10000)
displaytime = int(data.get(ATTR_DISPLAYTIME, 10000))
icon = data.get(ATTR_ICON, "info")
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
await self._server.GUI.ShowNotification(

View File

@@ -16,7 +16,7 @@ from homeassistant.setup import async_when_setup
from .config_flow import CONF_SECRET
REQUIREMENTS = ['libnacl==1.6.1']
REQUIREMENTS = ['PyNaCl==1.3.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -9,7 +9,7 @@ CONF_SECRET = 'secret'
def supports_encryption():
"""Test if we support encryption."""
try:
import libnacl # noqa pylint: disable=unused-import
import nacl # noqa pylint: disable=unused-import
return True
except OSError:
return False

View File

@@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over MQTT.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/
"""
import base64
import json
import logging
@@ -37,13 +36,13 @@ def get_cipher():
Async friendly.
"""
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
from libnacl.secret import SecretBox
from nacl.secret import SecretBox
from nacl.encoding import Base64Encoder
def decrypt(ciphertext, key):
"""Decrypt ciphertext using key."""
return SecretBox(key).decrypt(ciphertext)
return (KEYLEN, decrypt)
return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder)
return (SecretBox.KEY_SIZE, decrypt)
def _parse_topic(topic, subscribe_topic):
@@ -141,7 +140,6 @@ def _decrypt_payload(secret, topic, ciphertext):
key = key.ljust(keylen, b'\0')
try:
ciphertext = base64.b64decode(ciphertext)
message = decrypt(ciphertext, key)
message = message.decode("utf-8")
_LOGGER.debug("Decrypted payload: %s", message)

View File

@@ -2,6 +2,7 @@
from collections import OrderedDict
from itertools import chain
import logging
from typing import Optional
import uuid
import voluptuous as vol
@@ -13,7 +14,7 @@ from homeassistant.const import (
ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY,
CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START,
STATE_UNKNOWN, STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME)
from homeassistant.core import callback, Event
from homeassistant.core import callback, Event, State
from homeassistant.auth import EVENT_USER_REMOVED
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
@@ -377,15 +378,10 @@ class Person(RestoreEntity):
"""Handle the device tracker state changes."""
self._update_state()
def _get_latest(self, prev, curr):
return curr \
if prev is None or curr.last_updated > prev.last_updated \
else prev
@callback
def _update_state(self):
"""Update the state."""
latest_home = latest_not_home = latest_gps = latest = None
latest_non_gps_home = latest_not_home = latest_gps = latest = None
for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []):
state = self.hass.states.get(entity_id)
@@ -393,14 +389,14 @@ class Person(RestoreEntity):
continue
if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS:
latest_gps = self._get_latest(latest_gps, state)
latest_gps = _get_latest(latest_gps, state)
elif state.state == STATE_HOME:
latest_home = self._get_latest(latest_home, state)
latest_non_gps_home = _get_latest(latest_non_gps_home, state)
elif state.state == STATE_NOT_HOME:
latest_not_home = self._get_latest(latest_not_home, state)
latest_not_home = _get_latest(latest_not_home, state)
if latest_home:
latest = latest_home
if latest_non_gps_home:
latest = latest_non_gps_home
elif latest_gps:
latest = latest_gps
else:
@@ -508,3 +504,10 @@ async def ws_delete_person(hass: HomeAssistantType,
manager = hass.data[DOMAIN] # type: PersonManager
await manager.async_delete_person(msg['person_id'])
connection.send_result(msg['id'])
def _get_latest(prev: Optional[State], curr: State):
"""Get latest state."""
if prev is None or curr.last_updated > prev.last_updated:
return curr
return prev

View File

@@ -318,6 +318,10 @@ class Recorder(threading.Thread):
CONNECT_RETRY_WAIT)
tries += 1
except exc.SQLAlchemyError:
updated = True
_LOGGER.exception("Error saving event: %s", event)
if not updated:
_LOGGER.error("Error in database update. Could not save "
"after %d tries. Giving up", tries)

View File

@@ -141,7 +141,7 @@ async def async_setup_platform(
"Using city, state, and country: %s, %s, %s", city, state, country)
location_id = ','.join((city, state, country))
data = AirVisualData(
Client(config[CONF_API_KEY], websession),
Client(websession, api_key=config[CONF_API_KEY]),
city=city,
state=state,
country=country,
@@ -152,7 +152,7 @@ async def async_setup_platform(
"Using latitude and longitude: %s, %s", latitude, longitude)
location_id = ','.join((str(latitude), str(longitude)))
data = AirVisualData(
Client(config[CONF_API_KEY], websession),
Client(websession, api_key=config[CONF_API_KEY]),
latitude=latitude,
longitude=longitude,
show_on_map=config[CONF_SHOW_ON_MAP],
@@ -278,11 +278,11 @@ class AirVisualData:
try:
if self.city and self.state and self.country:
resp = await self._client.data.city(
resp = await self._client.api.city(
self.city, self.state, self.country)
self.longitude, self.latitude = resp['location']['coordinates']
else:
resp = await self._client.data.nearest_city(
resp = await self._client.api.nearest_city(
self.latitude, self.longitude)
_LOGGER.debug("New data retrieved: %s", resp)

View File

@@ -67,7 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}))
})
TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone']
TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone', 'person']
DATA_KEY = 'google_travel_time'

View File

@@ -48,10 +48,20 @@ async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""
from pysmartthings import SmartThings
# Delete the installed app
# Remove the installed_app, which if already removed raises a 403 error.
api = SmartThings(async_get_clientsession(hass),
entry.data[CONF_ACCESS_TOKEN])
await api.delete_installed_app(entry.data[CONF_INSTALLED_APP_ID])
installed_app_id = entry.data[CONF_INSTALLED_APP_ID]
try:
await api.delete_installed_app(installed_app_id)
except ClientResponseError as ex:
if ex.status == 403:
_LOGGER.exception("Installed app %s has already been removed",
installed_app_id)
else:
raise
_LOGGER.debug("Removed installed app %s", installed_app_id)
# Delete the entry
hass.async_create_task(
hass.config_entries.async_remove(entry.entry_id))

View File

@@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow
DOMAIN = 'sonos'
REQUIREMENTS = ['pysonos==0.0.7']
REQUIREMENTS = ['pysonos==0.0.8']
async def async_setup(hass, config):

View File

@@ -197,9 +197,11 @@ def _setup_platform(hass, config, add_entities, discovery_info):
with hass.data[DATA_SONOS].topology_lock:
if service.service == SERVICE_SNAPSHOT:
snapshot(entities, service.data[ATTR_WITH_GROUP])
SonosEntity.snapshot_multi(
entities, service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_RESTORE:
restore(entities, service.data[ATTR_WITH_GROUP])
SonosEntity.restore_multi(
entities, service.data[ATTR_WITH_GROUP])
elif service.service == SERVICE_JOIN:
master = [e for e in hass.data[DATA_SONOS].entities
if e.entity_id == service.data[ATTR_MASTER]]
@@ -357,6 +359,7 @@ class SonosEntity(MediaPlayerDevice):
self._favorites = None
self._soco_snapshot = None
self._snapshot_group = None
self._restore_pending = False
self._set_basic_information()
@@ -724,6 +727,9 @@ class SonosEntity(MediaPlayerDevice):
pass
if self.unique_id == coordinator_uid:
if self._restore_pending:
self.restore()
sonos_group = []
for uid in (coordinator_uid, *slave_uids):
entity = _get_entity_from_soco_uid(self.hass, uid)
@@ -974,6 +980,82 @@ class SonosEntity(MediaPlayerDevice):
self.soco.unjoin()
self._coordinator = None
@soco_error()
def snapshot(self, with_group):
"""Snapshot the state of a player."""
from pysonos.snapshot import Snapshot
self._soco_snapshot = Snapshot(self.soco)
self._soco_snapshot.snapshot()
if with_group:
self._snapshot_group = self._sonos_group.copy()
else:
self._snapshot_group = None
@soco_error()
def restore(self):
"""Restore a snapshotted state to a player."""
from pysonos.exceptions import SoCoException
try:
# pylint: disable=protected-access
self.soco._zgs_cache.clear()
self._soco_snapshot.restore()
except (TypeError, AttributeError, SoCoException) as ex:
# Can happen if restoring a coordinator onto a current slave
_LOGGER.warning("Error on restore %s: %s", self.entity_id, ex)
self._soco_snapshot = None
self._snapshot_group = None
self._restore_pending = False
@staticmethod
def snapshot_multi(entities, with_group):
"""Snapshot all the entities and optionally their groups."""
# pylint: disable=protected-access
# Find all affected players
entities = set(entities)
if with_group:
for entity in list(entities):
entities.update(entity._sonos_group)
for entity in entities:
entity.snapshot(with_group)
@staticmethod
def restore_multi(entities, with_group):
"""Restore snapshots for all the entities."""
# pylint: disable=protected-access
# Find all affected players
entities = set(e for e in entities if e._soco_snapshot)
if with_group:
for entity in [e for e in entities if e._snapshot_group]:
entities.update(entity._snapshot_group)
# Pause all current coordinators
for entity in (e for e in entities if e.is_coordinator):
if entity.state == STATE_PLAYING:
entity.media_pause()
# Bring back the original group topology
if with_group:
for entity in (e for e in entities if e._snapshot_group):
if entity._snapshot_group[0] == entity:
entity.join(entity._snapshot_group)
# Restore slaves
for entity in (e for e in entities if not e.is_coordinator):
entity.restore()
# Restore coordinators (or delay if moving from slave)
for entity in (e for e in entities if e.is_coordinator):
if entity._sonos_group[0] == entity:
# Was already coordinator
entity.restore()
else:
# Await coordinator role
entity._restore_pending = True
@soco_error()
@soco_coordinator
def set_sleep_timer(self, sleep_time):
@@ -1033,62 +1115,3 @@ class SonosEntity(MediaPlayerDevice):
attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance
return attributes
@soco_error()
def snapshot(entities, with_group):
"""Snapshot all the entities and optionally their groups."""
# pylint: disable=protected-access
from pysonos.snapshot import Snapshot
# Find all affected players
entities = set(entities)
if with_group:
for entity in list(entities):
entities.update(entity._sonos_group)
for entity in entities:
entity._soco_snapshot = Snapshot(entity.soco)
entity._soco_snapshot.snapshot()
if with_group:
entity._snapshot_group = entity._sonos_group.copy()
else:
entity._snapshot_group = None
@soco_error()
def restore(entities, with_group):
"""Restore snapshots for all the entities."""
# pylint: disable=protected-access
from pysonos.exceptions import SoCoException
# Find all affected players
entities = set(e for e in entities if e._soco_snapshot)
if with_group:
for entity in [e for e in entities if e._snapshot_group]:
entities.update(entity._snapshot_group)
# Pause all current coordinators
for entity in (e for e in entities if e.is_coordinator):
if entity.state == STATE_PLAYING:
entity.media_pause()
# Bring back the original group topology and clear pysonos cache
if with_group:
for entity in (e for e in entities if e._snapshot_group):
if entity._snapshot_group[0] == entity:
entity.join(entity._snapshot_group)
entity.soco._zgs_cache.clear()
# Restore slaves, then coordinators
slaves = [e for e in entities if not e.is_coordinator]
coordinators = [e for e in entities if e.is_coordinator]
for entity in slaves + coordinators:
try:
entity._soco_snapshot.restore()
except (TypeError, AttributeError, SoCoException) as ex:
# Can happen if restoring a coordinator onto a current slave
_LOGGER.warning("Error on restore %s: %s", entity.entity_id, ex)
entity._soco_snapshot = None
entity._snapshot_group = None

View File

@@ -91,15 +91,15 @@ class LogEntry:
self.first_occured = self.timestamp = record.created
self.level = record.levelname
self.message = record.getMessage()
self.exception = ''
self.root_cause = None
if record.exc_info:
self.exception = ''.join(
traceback.format_exception(*record.exc_info))
_, _, tb = record.exc_info # pylint: disable=invalid-name
# Last line of traceback contains the root cause of the exception
self.root_cause = str(traceback.extract_tb(tb)[-1])
else:
self.exception = ''
self.root_cause = None
if traceback.extract_tb(tb):
self.root_cause = str(traceback.extract_tb(tb)[-1])
self.source = source
self.count = 1

View File

@@ -84,6 +84,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PROXY_PARAMS): dict,
})
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema)
BASE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(ATTR_PARSER): cv.string,

View File

@@ -44,11 +44,14 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
def close_cover(self, **kwargs):
"""Close the cover."""
self.device.down()
self._update_callback()
def open_cover(self, **kwargs):
"""Open the cover."""
self.device.up()
self._update_callback()
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.device.stop()
self._update_callback()

View File

@@ -45,6 +45,7 @@ class TelldusLiveLight(TelldusLiveEntity, Light):
def changed(self):
"""Define a property of the device that might have changed."""
self._last_brightness = self.brightness
self._update_callback()
@property
def brightness(self):

View File

@@ -44,7 +44,9 @@ class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity):
def turn_on(self, **kwargs):
"""Turn the switch on."""
self.device.turn_on()
self._update_callback()
def turn_off(self, **kwargs):
"""Turn the switch off."""
self.device.turn_off()
self._update_callback()

View File

@@ -1,6 +1,7 @@
"""Support for Toon van Eneco devices."""
import logging
from typing import Any, Dict
from functools import partial
import voluptuous as vol
@@ -15,7 +16,7 @@ from .const import (
CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT,
DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN)
REQUIREMENTS = ['toonapilib==3.0.9']
REQUIREMENTS = ['toonapilib==3.2.1']
_LOGGER = logging.getLogger(__name__)
@@ -48,10 +49,11 @@ async def async_setup_entry(hass: HomeAssistantType,
conf = hass.data.get(DATA_TOON_CONFIG)
toon = Toon(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD],
conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET],
tenant_id=entry.data[CONF_TENANT],
display_common_name=entry.data[CONF_DISPLAY])
toon = await hass.async_add_executor_job(partial(
Toon, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD],
conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET],
tenant_id=entry.data[CONF_TENANT],
display_common_name=entry.data[CONF_DISPLAY]))
hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon

View File

@@ -102,7 +102,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorDevice):
return value
async def async_update(self) -> None:
def update(self) -> None:
"""Get the latest data from the binary sensor."""
section = getattr(self.toon, self.section)
self._state = getattr(section, self.measurement)

View File

@@ -117,7 +117,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice):
"""Set new operation mode."""
self.toon.thermostat_state = HA_TOON[operation_mode]
async def async_update(self) -> None:
def update(self) -> None:
"""Update local state."""
if self.toon.thermostat_state is None:
self._state = None

View File

@@ -1,6 +1,7 @@
"""Config flow to configure the Toon component."""
from collections import OrderedDict
import logging
from functools import partial
import voluptuous as vol
@@ -75,11 +76,10 @@ class ToonFlowHandler(config_entries.ConfigFlow):
app = self.hass.data.get(DATA_TOON_CONFIG, {})
try:
toon = Toon(user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
app[CONF_CLIENT_ID],
app[CONF_CLIENT_SECRET],
tenant_id=user_input[CONF_TENANT])
toon = await self.hass.async_add_executor_job(partial(
Toon, user_input[CONF_USERNAME], user_input[CONF_PASSWORD],
app[CONF_CLIENT_ID], app[CONF_CLIENT_SECRET],
tenant_id=user_input[CONF_TENANT]))
displays = toon.display_names
@@ -136,12 +136,10 @@ class ToonFlowHandler(config_entries.ConfigFlow):
app = self.hass.data.get(DATA_TOON_CONFIG, {})
try:
Toon(self.username,
self.password,
app[CONF_CLIENT_ID],
app[CONF_CLIENT_SECRET],
tenant_id=self.tenant,
display_common_name=user_input[CONF_DISPLAY])
await self.hass.async_add_executor_job(partial(
Toon, self.username, self.password, app[CONF_CLIENT_ID],
app[CONF_CLIENT_SECRET], tenant_id=self.tenant,
display_common_name=user_input[CONF_DISPLAY]))
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error while authenticating")

View File

@@ -14,7 +14,7 @@ DEFAULT_MAX_TEMP = 30.0
DEFAULT_MIN_TEMP = 6.0
CURRENCY_EUR = 'EUR'
POWER_WATT = 'Watt'
POWER_WATT = 'W'
POWER_KWH = 'kWh'
RATIO_PERCENT = '%'
VOLUME_CM3 = 'CM3'

View File

@@ -134,7 +134,7 @@ class ToonSensor(ToonEntity):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
async def async_update(self) -> None:
def update(self) -> None:
"""Get the latest data from the sensor."""
section = getattr(self.toon, self.section)
value = None

View File

@@ -34,7 +34,7 @@ def async_setup_platform(hass, config, add_entities, discovery_info=None):
Deprecated.
"""
_LOGGER.warning('Loading as a platform is deprecated, '
_LOGGER.warning('Loading as a platform is no longer supported, '
'convert to use the tplink component.')

View File

@@ -29,7 +29,7 @@ def async_setup_platform(hass, config, add_entities, discovery_info=None):
Deprecated.
"""
_LOGGER.warning('Loading as a platform is deprecated, '
_LOGGER.warning('Loading as a platform is no longer supported, '
'convert to use the tplink component.')

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyeconet==0.0.8']
REQUIREMENTS = ['pyeconet==0.0.9']
_LOGGER = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ from homeassistant.components.media_player.const import (
MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP)
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP)
from homeassistant.const import (
CONF_CUSTOMIZE, CONF_FILENAME, CONF_HOST, CONF_NAME, CONF_TIMEOUT,
STATE_OFF, STATE_PAUSED, STATE_PLAYING)
@@ -36,7 +36,7 @@ WEBOSTV_CONFIG_FILE = 'webostv.conf'
SUPPORT_WEBOSTV = SUPPORT_TURN_OFF | \
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)

View File

@@ -3,7 +3,8 @@ import voluptuous as vol
from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED
from homeassistant.core import callback, DOMAIN as HASS_DOMAIN
from homeassistant.exceptions import Unauthorized, ServiceNotFound
from homeassistant.exceptions import Unauthorized, ServiceNotFound, \
HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_get_all_descriptions
@@ -149,6 +150,14 @@ async def handle_call_service(hass, connection, msg):
except ServiceNotFound:
connection.send_message(messages.error_message(
msg['id'], const.ERR_NOT_FOUND, 'Service not found.'))
except HomeAssistantError as err:
connection.logger.exception(err)
connection.send_message(messages.error_message(
msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err)))
except Exception as err: # pylint: disable=broad-except
connection.logger.exception(err)
connection.send_message(messages.error_message(
msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err)))
@callback

View File

@@ -9,6 +9,7 @@ MAX_PENDING_MSG = 512
ERR_ID_REUSE = 'id_reuse'
ERR_INVALID_FORMAT = 'invalid_format'
ERR_NOT_FOUND = 'not_found'
ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error'
ERR_UNKNOWN_COMMAND = 'unknown_command'
ERR_UNKNOWN_ERROR = 'unknown_error'
ERR_UNAUTHORIZED = 'unauthorized'

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['PyXiaomiGateway==0.11.2']
REQUIREMENTS = ['PyXiaomiGateway==0.12.2']
_LOGGER = logging.getLogger(__name__)

View File

@@ -33,7 +33,7 @@ REQUIREMENTS = [
'zigpy-homeassistant==0.3.0',
'zigpy-xbee-homeassistant==0.1.2',
'zha-quirks==0.0.6',
'zigpy-deconz==0.1.1'
'zigpy-deconz==0.1.2'
]
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({

View File

@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 89
PATCH_VERSION = '0b0'
PATCH_VERSION = '1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@@ -370,7 +370,7 @@ def async_track_utc_time_change(hass, action,
last_now = now
if next_time <= now:
hass.async_run_job(action, event.data[ATTR_NOW])
hass.async_run_job(action, dt_util.as_local(now) if local else now)
calculate_next(now + timedelta(seconds=1))
# We can't use async_track_point_in_utc_time here because it would

View File

@@ -272,7 +272,10 @@ async def entity_service_call(hass, platforms, func, call, service_name=''):
]
if tasks:
await asyncio.wait(tasks)
done, pending = await asyncio.wait(tasks)
assert not pending
for future in done:
future.result() # pop exception if have
async def _handle_service_platform_call(func, data, entities, context):
@@ -294,4 +297,7 @@ async def _handle_service_platform_call(func, data, entities, context):
tasks.append(entity.async_update_ha_state(True))
if tasks:
await asyncio.wait(tasks)
done, pending = await asyncio.wait(tasks)
assert not pending
for future in done:
future.result() # pop exception if have

View File

@@ -52,15 +52,10 @@ def run(args: List) -> int:
hass = HomeAssistant(loop)
pkgload = PackageLoadable(hass)
for req in getattr(script, 'REQUIREMENTS', []):
try:
loop.run_until_complete(pkgload.loadable(req))
if loop.run_until_complete(pkgload.loadable(req)):
continue
except ImportError:
pass
returncode = install_package(req, **_pip_kwargs)
if not returncode:
if not install_package(req, **_pip_kwargs):
print('Aborting script, could not install dependency', req)
return 1

View File

@@ -50,6 +50,10 @@ PyMVGLive==1.1.4
# homeassistant.components.arduino
PyMata==2.14
# homeassistant.components.mobile_app
# homeassistant.components.owntracks
PyNaCl==1.3.0
# homeassistant.auth.mfa_modules.totp
PyQRCode==1.2.1
@@ -63,7 +67,7 @@ PyRMVtransport==0.1.3
PyTransportNSW==0.1.1
# homeassistant.components.xiaomi_aqara
PyXiaomiGateway==0.11.2
PyXiaomiGateway==0.12.2
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.5
@@ -202,7 +206,7 @@ bellows-homeassistant==0.7.1
bimmer_connected==0.5.3
# homeassistant.components.blink
blinkpy==0.12.1
blinkpy==0.13.1
# homeassistant.components.light.blinksticklight
blinkstick==1.1.8
@@ -535,7 +539,7 @@ hole==0.3.0
holidays==0.9.9
# homeassistant.components.frontend
home-assistant-frontend==20190227.0
home-assistant-frontend==20190305.1
# homeassistant.components.zwave
homeassistant-pyozw==0.1.2
@@ -608,9 +612,6 @@ konnected==0.1.4
# homeassistant.components.eufy
lakeside==0.12
# homeassistant.components.owntracks
libnacl==1.6.1
# homeassistant.components.dyson
libpurecoollink==0.4.2
@@ -1003,7 +1004,7 @@ pydukeenergy==0.0.6
pyebox==1.1.4
# homeassistant.components.water_heater.econet
pyeconet==0.0.8
pyeconet==0.0.9
# homeassistant.components.switch.edimax
pyedimax==0.1
@@ -1266,7 +1267,7 @@ pysmartthings==0.6.3
pysnmp==4.4.8
# homeassistant.components.sonos
pysonos==0.0.7
pysonos==0.0.8
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1688,7 +1689,7 @@ tikteck==0.4
todoist-python==7.0.17
# homeassistant.components.toon
toonapilib==3.0.9
toonapilib==3.2.1
# homeassistant.components.alarm_control_panel.totalconnect
total_connect_client==0.22
@@ -1822,7 +1823,7 @@ zhong_hong_hvac==1.0.9
ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha
zigpy-deconz==0.1.1
zigpy-deconz==0.1.2
# homeassistant.components.zha
zigpy-homeassistant==0.3.0

View File

@@ -21,6 +21,10 @@ requests_mock==1.5.2
# homeassistant.components.homekit
HAP-python==2.4.2
# homeassistant.components.mobile_app
# homeassistant.components.owntracks
PyNaCl==1.3.0
# homeassistant.components.sensor.rmvtransport
PyRMVtransport==0.1.3
@@ -116,7 +120,7 @@ hdate==0.8.7
holidays==0.9.9
# homeassistant.components.frontend
home-assistant-frontend==20190227.0
home-assistant-frontend==20190305.1
# homeassistant.components.homekit_controller
homekit==0.12.2
@@ -226,7 +230,7 @@ pysmartapp==0.3.0
pysmartthings==0.6.3
# homeassistant.components.sonos
pysonos==0.0.7
pysonos==0.0.8
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -291,7 +295,7 @@ srpenergy==1.0.5
statsd==3.2.1
# homeassistant.components.toon
toonapilib==3.0.9
toonapilib==3.2.1
# homeassistant.components.camera.uvc
uvcclient==0.11.0

View File

@@ -108,6 +108,7 @@ TEST_REQUIREMENTS = (
'pyupnp-async',
'pywebpush',
'pyHS100',
'PyNaCl',
'regenmaschine',
'restrictedpython',
'rflink',

View File

@@ -1,4 +1,5 @@
"""Test the HMAC-based One Time Password (MFA) auth module."""
import asyncio
from unittest.mock import patch
from homeassistant import data_entry_flow
@@ -395,3 +396,26 @@ async def test_not_raise_exception_when_service_not_exist(hass):
# wait service call finished
await hass.async_block_till_done()
async def test_race_condition_in_data_loading(hass):
"""Test race condition in the data loading."""
counter = 0
async def mock_load(_):
"""Mock homeassistant.helpers.storage.Store.async_load."""
nonlocal counter
counter += 1
await asyncio.sleep(0)
notify_auth_module = await auth_mfa_module_from_config(hass, {
'type': 'notify'
})
with patch('homeassistant.helpers.storage.Store.async_load',
new=mock_load):
task1 = notify_auth_module.async_validate('user', {'code': 'value'})
task2 = notify_auth_module.async_validate('user', {'code': 'value'})
results = await asyncio.gather(task1, task2, return_exceptions=True)
assert counter == 1
assert results[0] is False
assert results[1] is False

View File

@@ -1,4 +1,5 @@
"""Test the Time-based One Time Password (MFA) auth module."""
import asyncio
from unittest.mock import patch
from homeassistant import data_entry_flow
@@ -128,3 +129,26 @@ async def test_login_flow_validates_mfa(hass):
result['flow_id'], {'code': MOCK_CODE})
assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result['data'].id == 'mock-user'
async def test_race_condition_in_data_loading(hass):
"""Test race condition in the data loading."""
counter = 0
async def mock_load(_):
"""Mock of homeassistant.helpers.storage.Store.async_load."""
nonlocal counter
counter += 1
await asyncio.sleep(0)
totp_auth_module = await auth_mfa_module_from_config(hass, {
'type': 'totp'
})
with patch('homeassistant.helpers.storage.Store.async_load',
new=mock_load):
task1 = totp_auth_module.async_validate('user', {'code': 'value'})
task2 = totp_auth_module.async_validate('user', {'code': 'value'})
results = await asyncio.gather(task1, task2, return_exceptions=True)
assert counter == 1
assert results[0] is False
assert results[1] is False

View File

@@ -1,4 +1,5 @@
"""Test the Home Assistant local auth provider."""
import asyncio
from unittest.mock import Mock, patch
import pytest
@@ -288,3 +289,29 @@ async def test_legacy_get_or_create_credentials(hass, legacy_data):
'username': 'hello '
})
assert credentials1 is not credentials3
async def test_race_condition_in_data_loading(hass):
"""Test race condition in the hass_auth.Data loading.
Ref issue: https://github.com/home-assistant/home-assistant/issues/21569
"""
counter = 0
async def mock_load(_):
"""Mock of homeassistant.helpers.storage.Store.async_load."""
nonlocal counter
counter += 1
await asyncio.sleep(0)
provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass),
{'type': 'homeassistant'})
with patch('homeassistant.helpers.storage.Store.async_load',
new=mock_load):
task1 = provider.async_validate_login('user', 'pass')
task2 = provider.async_validate_login('user', 'pass')
results = await asyncio.gather(task1, task2, return_exceptions=True)
assert counter == 1
assert isinstance(results[0], hass_auth.InvalidAuth)
# results[1] will be a TypeError if race condition occurred
assert isinstance(results[1], hass_auth.InvalidAuth)

View File

@@ -1,6 +1,8 @@
"""deCONZ climate platform tests."""
from unittest.mock import Mock, patch
import asynctest
from homeassistant import config_entries
from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -43,8 +45,14 @@ ENTRY_CONFIG = {
async def setup_gateway(hass, data, allow_clip_sensor=True):
"""Load the deCONZ sensor platform."""
from pydeconz import DeconzSession
loop = Mock()
session = Mock()
session = Mock(put=asynctest.CoroutineMock(
return_value=Mock(status=200,
json=asynctest.CoroutineMock(),
text=asynctest.CoroutineMock(),
)
)
)
ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor
@@ -52,7 +60,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True):
1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
config_entries.CONN_CLASS_LOCAL_PUSH)
gateway = deconz.DeconzGateway(hass, config_entry)
gateway.api = DeconzSession(loop, session, **config_entry.data)
gateway.api = DeconzSession(hass.loop, session, **config_entry.data)
gateway.api.config = Mock()
hass.data[deconz.DOMAIN] = gateway

View File

@@ -1295,18 +1295,25 @@ async def test_unsupported_message(hass, context):
def generate_ciphers(secret):
"""Generate test ciphers for the DEFAULT_LOCATION_MESSAGE."""
# libnacl ciphertext generation will fail if the module
# PyNaCl ciphertext generation will fail if the module
# cannot be imported. However, the test for decryption
# also relies on this library and won't be run without it.
import pickle
import base64
try:
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
from libnacl.secret import SecretBox
key = secret.encode("utf-8")[:KEYLEN].ljust(KEYLEN, b'\0')
ctxt = base64.b64encode(SecretBox(key).encrypt(json.dumps(
DEFAULT_LOCATION_MESSAGE).encode("utf-8"))).decode("utf-8")
from nacl.secret import SecretBox
from nacl.encoding import Base64Encoder
keylen = SecretBox.KEY_SIZE
key = secret.encode("utf-8")
key = key[:keylen]
key = key.ljust(keylen, b'\0')
msg = json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8")
ctxt = SecretBox(key).encrypt(msg,
encoder=Base64Encoder).decode("utf-8")
except (ImportError, OSError):
ctxt = ''
@@ -1341,7 +1348,8 @@ def mock_cipher():
def mock_decrypt(ciphertext, key):
"""Decrypt/unpickle."""
import pickle
(mkey, plaintext) = pickle.loads(ciphertext)
import base64
(mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext))
if key != mkey:
raise ValueError()
return plaintext
@@ -1443,9 +1451,9 @@ async def test_encrypted_payload_libsodium(hass, setup_comp):
"""Test sending encrypted message payload."""
try:
# pylint: disable=unused-import
import libnacl # noqa: F401
import nacl # noqa: F401
except (ImportError, OSError):
pytest.skip("libnacl/libsodium is not installed")
pytest.skip("PyNaCl/libsodium is not installed")
return
await setup_owntracks(hass, {

View File

@@ -0,0 +1 @@
"""Tests for mobile_app component."""

View File

@@ -0,0 +1,275 @@
"""Test the mobile_app_http platform."""
import pytest
from homeassistant.setup import async_setup_component
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.components.mobile_app import (DOMAIN, STORAGE_KEY,
STORAGE_VERSION,
CONF_SECRET, CONF_USER_ID)
from homeassistant.core import callback
from tests.common import async_mock_service
FIRE_EVENT = {
'type': 'fire_event',
'data': {
'event_type': 'test_event',
'event_data': {
'hello': 'yo world'
}
}
}
RENDER_TEMPLATE = {
'type': 'render_template',
'data': {
'template': 'Hello world'
}
}
CALL_SERVICE = {
'type': 'call_service',
'data': {
'domain': 'test',
'service': 'mobile_app',
'service_data': {
'foo': 'bar'
}
}
}
REGISTER = {
'app_data': {'foo': 'bar'},
'app_id': 'io.homeassistant.mobile_app_test',
'app_name': 'Mobile App Tests',
'app_version': '1.0.0',
'device_name': 'Test 1',
'manufacturer': 'mobile_app',
'model': 'Test',
'os_version': '1.0',
'supports_encryption': True
}
UPDATE = {
'app_data': {'foo': 'bar'},
'app_version': '2.0.0',
'device_name': 'Test 1',
'manufacturer': 'mobile_app',
'model': 'Test',
'os_version': '1.0'
}
# pylint: disable=redefined-outer-name
@pytest.fixture
def mobile_app_client(hass, aiohttp_client, hass_storage, hass_admin_user):
"""mobile_app mock client."""
hass_storage[STORAGE_KEY] = {
'version': STORAGE_VERSION,
'data': {
'mobile_app_test': {
CONF_SECRET: '58eb127991594dad934d1584bdee5f27',
'supports_encryption': True,
CONF_WEBHOOK_ID: 'mobile_app_test',
'device_name': 'Test Device',
CONF_USER_ID: hass_admin_user.id,
}
}
}
assert hass.loop.run_until_complete(async_setup_component(
hass, DOMAIN, {
DOMAIN: {}
}))
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
@pytest.fixture
async def mock_api_client(hass, hass_client):
"""Provide an authenticated client for mobile_app to use."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
return await hass_client()
async def test_handle_render_template(mobile_app_client):
"""Test that we render templates properly."""
resp = await mobile_app_client.post(
'/api/webhook/mobile_app_test',
json=RENDER_TEMPLATE
)
assert resp.status == 200
json = await resp.json()
assert json == {'rendered': 'Hello world'}
async def test_handle_call_services(hass, mobile_app_client):
"""Test that we call services properly."""
calls = async_mock_service(hass, 'test', 'mobile_app')
resp = await mobile_app_client.post(
'/api/webhook/mobile_app_test',
json=CALL_SERVICE
)
assert resp.status == 200
assert len(calls) == 1
async def test_handle_fire_event(hass, mobile_app_client):
"""Test that we can fire events."""
events = []
@callback
def store_event(event):
"""Helepr to store events."""
events.append(event)
hass.bus.async_listen('test_event', store_event)
resp = await mobile_app_client.post(
'/api/webhook/mobile_app_test',
json=FIRE_EVENT
)
assert resp.status == 200
text = await resp.text()
assert text == ""
assert len(events) == 1
assert events[0].data['hello'] == 'yo world'
async def test_update_registration(mobile_app_client, hass_client):
"""Test that a we can update an existing registration via webhook."""
mock_api_client = await hass_client()
register_resp = await mock_api_client.post(
'/api/mobile_app/devices', json=REGISTER
)
assert register_resp.status == 201
register_json = await register_resp.json()
webhook_id = register_json[CONF_WEBHOOK_ID]
update_container = {
'type': 'update_registration',
'data': UPDATE
}
update_resp = await mobile_app_client.post(
'/api/webhook/{}'.format(webhook_id), json=update_container
)
assert update_resp.status == 200
update_json = await update_resp.json()
assert update_json['app_version'] == '2.0.0'
assert CONF_WEBHOOK_ID not in update_json
assert CONF_SECRET not in update_json
async def test_returns_error_incorrect_json(mobile_app_client, caplog):
"""Test that an error is returned when JSON is invalid."""
resp = await mobile_app_client.post(
'/api/webhook/mobile_app_test',
data='not json'
)
assert resp.status == 400
json = await resp.json()
assert json == []
assert 'invalid JSON' in caplog.text
async def test_handle_decryption(mobile_app_client):
"""Test that we can encrypt/decrypt properly."""
try:
# pylint: disable=unused-import
from nacl.secret import SecretBox # noqa: F401
from nacl.encoding import Base64Encoder # noqa: F401
except (ImportError, OSError):
pytest.skip("libnacl/libsodium is not installed")
return
import json
keylen = SecretBox.KEY_SIZE
key = "58eb127991594dad934d1584bdee5f27".encode("utf-8")
key = key[:keylen]
key = key.ljust(keylen, b'\0')
payload = json.dumps({'template': 'Hello world'}).encode("utf-8")
data = SecretBox(key).encrypt(payload,
encoder=Base64Encoder).decode("utf-8")
container = {
'type': 'render_template',
'encrypted': True,
'encrypted_data': data,
}
resp = await mobile_app_client.post(
'/api/webhook/mobile_app_test',
json=container
)
assert resp.status == 200
json = await resp.json()
assert json == {'rendered': 'Hello world'}
async def test_register_device(hass_client, mock_api_client):
"""Test that a device can be registered."""
try:
# pylint: disable=unused-import
from nacl.secret import SecretBox # noqa: F401
from nacl.encoding import Base64Encoder # noqa: F401
except (ImportError, OSError):
pytest.skip("libnacl/libsodium is not installed")
return
import json
resp = await mock_api_client.post(
'/api/mobile_app/devices', json=REGISTER
)
assert resp.status == 201
register_json = await resp.json()
assert CONF_WEBHOOK_ID in register_json
assert CONF_SECRET in register_json
keylen = SecretBox.KEY_SIZE
key = register_json[CONF_SECRET].encode("utf-8")
key = key[:keylen]
key = key.ljust(keylen, b'\0')
payload = json.dumps({'template': 'Hello world'}).encode("utf-8")
data = SecretBox(key).encrypt(payload,
encoder=Base64Encoder).decode("utf-8")
container = {
'type': 'render_template',
'encrypted': True,
'encrypted_data': data,
}
mobile_app_client = await hass_client()
resp = await mobile_app_client.post(
'/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]),
json=container
)
assert resp.status == 200
webhook_json = await resp.json()
assert webhook_json == {'rendered': 'Hello world'}

View File

@@ -328,7 +328,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
snapshotMock.return_value = True
entity.soco.group = mock.MagicMock()
entity.soco.group.members = [e.soco for e in entities]
sonos.snapshot(entities, True)
sonos.SonosEntity.snapshot_multi(entities, True)
assert snapshotMock.call_count == 1
assert snapshotMock.call_args == mock.call()
@@ -350,6 +350,6 @@ class TestSonosMediaPlayer(unittest.TestCase):
entity._snapshot_group = mock.MagicMock()
entity._snapshot_group.members = [e.soco for e in entities]
entity._soco_snapshot = Snapshot(entity.soco)
sonos.restore(entities, True)
sonos.SonosEntity.restore_multi(entities, True)
assert restoreMock.call_count == 1
assert restoreMock.call_args == mock.call()

View File

@@ -7,6 +7,7 @@ from homeassistant.components.websocket_api.auth import (
TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED
)
from homeassistant.components.websocket_api import const, commands
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
@@ -66,6 +67,51 @@ async def test_call_service_not_found(hass, websocket_client):
assert msg['error']['code'] == const.ERR_NOT_FOUND
async def test_call_service_error(hass, websocket_client):
"""Test call service command with error."""
@callback
def ha_error_call(_):
raise HomeAssistantError('error_message')
hass.services.async_register('domain_test', 'ha_error', ha_error_call)
async def unknown_error_call(_):
raise ValueError('value_error')
hass.services.async_register(
'domain_test', 'unknown_error', unknown_error_call)
await websocket_client.send_json({
'id': 5,
'type': commands.TYPE_CALL_SERVICE,
'domain': 'domain_test',
'service': 'ha_error',
})
msg = await websocket_client.receive_json()
print(msg)
assert msg['id'] == 5
assert msg['type'] == const.TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'home_assistant_error'
assert msg['error']['message'] == 'error_message'
await websocket_client.send_json({
'id': 6,
'type': commands.TYPE_CALL_SERVICE,
'domain': 'domain_test',
'service': 'unknown_error',
})
msg = await websocket_client.receive_json()
print(msg)
assert msg['id'] == 6
assert msg['type'] == const.TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'unknown_error'
assert msg['error']['message'] == 'value_error'
async def test_subscribe_unsubscribe_events(hass, websocket_client):
"""Test subscribe/unsubscribe events command."""
init_count = sum(hass.bus.async_listeners().values())

View File

@@ -726,8 +726,7 @@ class TestServiceRegistry(unittest.TestCase):
"""Test registering and calling an async service."""
calls = []
@asyncio.coroutine
def service_handler(call):
async def service_handler(call):
"""Service handler coroutine."""
calls.append(call)
@@ -803,6 +802,45 @@ class TestServiceRegistry(unittest.TestCase):
self.hass.block_till_done()
assert len(calls_remove) == 0
def test_async_service_raise_exception(self):
"""Test registering and calling an async service raise exception."""
async def service_handler(_):
"""Service handler coroutine."""
raise ValueError
self.services.register(
'test_domain', 'register_calls', service_handler)
self.hass.block_till_done()
with pytest.raises(ValueError):
assert self.services.call('test_domain', 'REGISTER_CALLS',
blocking=True)
self.hass.block_till_done()
# Non-blocking service call never throw exception
self.services.call('test_domain', 'REGISTER_CALLS', blocking=False)
self.hass.block_till_done()
def test_callback_service_raise_exception(self):
"""Test registering and calling an callback service raise exception."""
@ha.callback
def service_handler(_):
"""Service handler coroutine."""
raise ValueError
self.services.register(
'test_domain', 'register_calls', service_handler)
self.hass.block_till_done()
with pytest.raises(ValueError):
assert self.services.call('test_domain', 'REGISTER_CALLS',
blocking=True)
self.hass.block_till_done()
# Non-blocking service call never throw exception
self.services.call('test_domain', 'REGISTER_CALLS', blocking=False)
self.hass.block_till_done()
class TestConfig(unittest.TestCase):
"""Test configuration methods."""