mirror of
https://github.com/home-assistant/core.git
synced 2026-01-05 23:35:24 +01:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a87b2e4e | ||
|
|
737c7e871d | ||
|
|
1f3e4c5776 | ||
|
|
00d01865cf | ||
|
|
1b4905ae5a | ||
|
|
39749952ee | ||
|
|
ac4d5d7c30 | ||
|
|
f3e8e34089 | ||
|
|
eae6d1c7a6 | ||
|
|
a121c92f52 | ||
|
|
4d6f21ecb2 | ||
|
|
1638d0a92f | ||
|
|
c031fd4164 | ||
|
|
5f0c37ccfc | ||
|
|
e412317194 | ||
|
|
44341a958a | ||
|
|
5a555102b9 | ||
|
|
aebe6ab70c | ||
|
|
48c9758cf5 | ||
|
|
279470613c | ||
|
|
88bc3033d3 | ||
|
|
21de636e5b | ||
|
|
87b5faa244 | ||
|
|
c2f4293c6a | ||
|
|
4c72f3c48b | ||
|
|
cb613984df | ||
|
|
4978a1681e | ||
|
|
2303e1684e | ||
|
|
3135257c0d | ||
|
|
b20b811cb9 | ||
|
|
a778cd117f | ||
|
|
31b88197eb | ||
|
|
81c252f917 | ||
|
|
f5a0b5ab98 | ||
|
|
a382ba731d | ||
|
|
cca8d4c951 | ||
|
|
932080656d | ||
|
|
d5bdfdb0b3 | ||
|
|
d9806f759b | ||
|
|
e6debe09e8 | ||
|
|
c5dad82211 | ||
|
|
ec9ccf6402 | ||
|
|
a268aab2ec | ||
|
|
996e0a6389 | ||
|
|
e877983533 | ||
|
|
73675d5a48 | ||
|
|
43f85f7053 |
@@ -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
41
.github/main.workflow
vendored
Normal 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"]
|
||||
}
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -119,6 +119,17 @@ class TodSensor(BinarySensorDevice):
|
||||
self.hass.config.time_zone).isoformat(),
|
||||
}
|
||||
|
||||
def _naive_time_to_utc_datetime(self, naive_time):
|
||||
"""Convert naive time from config to utc_datetime with current day."""
|
||||
# get the current local date from utc time
|
||||
current_local_date = self.current_datetime.astimezone(
|
||||
self.hass.config.time_zone).date()
|
||||
# calcuate utc datetime corecponding to local time
|
||||
utc_datetime = self.hass.config.time_zone.localize(
|
||||
datetime.combine(
|
||||
current_local_date, naive_time)).astimezone(tz=pytz.UTC)
|
||||
return utc_datetime
|
||||
|
||||
def _calculate_initial_boudary_time(self):
|
||||
"""Calculate internal absolute time boudaries."""
|
||||
nowutc = self.current_datetime
|
||||
@@ -134,9 +145,7 @@ class TodSensor(BinarySensorDevice):
|
||||
# datetime.combine(date, time, tzinfo) is not supported
|
||||
# in python 3.5. The self._after is provided
|
||||
# with hass configured TZ not system wide
|
||||
after_event_date = datetime.combine(
|
||||
nowutc, self._after.replace(
|
||||
tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC)
|
||||
after_event_date = self._naive_time_to_utc_datetime(self._after)
|
||||
|
||||
self._time_after = after_event_date
|
||||
|
||||
@@ -154,9 +163,7 @@ class TodSensor(BinarySensorDevice):
|
||||
self.hass, self._before, after_event_date)
|
||||
else:
|
||||
# Convert local time provided to UTC today, see above
|
||||
before_event_date = datetime.combine(
|
||||
nowutc, self._before.replace(
|
||||
tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC)
|
||||
before_event_date = self._naive_time_to_utc_datetime(self._before)
|
||||
|
||||
# It is safe to add timedelta days=1 to UTC as there is no DST
|
||||
if before_event_date < after_event_date + self._after_offset:
|
||||
@@ -190,7 +197,6 @@ class TodSensor(BinarySensorDevice):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
self._calculate_initial_boudary_time()
|
||||
self._calculate_next_update()
|
||||
self._point_in_time_listener(dt_util.now())
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -11,6 +11,7 @@ DEPENDENCIES = (
|
||||
'history',
|
||||
'logbook',
|
||||
'map',
|
||||
'mobile_app',
|
||||
'person',
|
||||
'script',
|
||||
'sun',
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass
|
||||
|
||||
from .storage import async_setup_frontend_storage
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20190228.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20190305.1']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 != \
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -211,6 +211,14 @@ class HomeAssistantHTTP:
|
||||
"legacy_api_password support has been enabled. If you don't "
|
||||
"require it, remove the 'api_password' from your http config.")
|
||||
|
||||
for prv in hass.auth.auth_providers:
|
||||
if prv.type == 'trusted_networks':
|
||||
# auth_provider.trusted_networks will override
|
||||
# http.trusted_networks, http.trusted_networks will be
|
||||
# removed from future release
|
||||
trusted_networks = prv.trusted_networks
|
||||
break
|
||||
|
||||
setup_auth(app, trusted_networks,
|
||||
api_password if hass.auth.support_legacy else None)
|
||||
|
||||
|
||||
355
homeassistant/components/mobile_app/__init__.py
Normal file
355
homeassistant/components/mobile_app/__init__.py
Normal 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)
|
||||
@@ -186,10 +186,13 @@ 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":
|
||||
if self._robot_maps[self._robot_serial]:
|
||||
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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from .smartapp import (
|
||||
setup_smartapp, setup_smartapp_endpoint, smartapp_sync_subscriptions,
|
||||
validate_installed_app)
|
||||
|
||||
REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.3']
|
||||
REQUIREMENTS = ['pysmartapp==0.3.1', 'pysmartthings==0.6.7']
|
||||
DEPENDENCIES = ['webhook']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -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))
|
||||
@@ -280,7 +290,8 @@ class DeviceBroker:
|
||||
if not device:
|
||||
continue
|
||||
device.status.apply_attribute_update(
|
||||
evt.component_id, evt.capability, evt.attribute, evt.value)
|
||||
evt.component_id, evt.capability, evt.attribute, evt.value,
|
||||
data=evt.data)
|
||||
|
||||
# Fire events for buttons
|
||||
if evt.capability == Capability.button and \
|
||||
@@ -290,7 +301,8 @@ class DeviceBroker:
|
||||
'device_id': evt.device_id,
|
||||
'location_id': evt.location_id,
|
||||
'value': evt.value,
|
||||
'name': device.label
|
||||
'name': device.label,
|
||||
'data': evt.data
|
||||
}
|
||||
self._hass.bus.async_fire(EVENT_BUTTON, data)
|
||||
_LOGGER.debug("Fired button event: %s", data)
|
||||
@@ -302,6 +314,7 @@ class DeviceBroker:
|
||||
'capability': evt.capability,
|
||||
'attribute': evt.attribute,
|
||||
'value': evt.value,
|
||||
'data': evt.data
|
||||
}
|
||||
_LOGGER.debug("Push update received: %s", data)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
|
||||
|
||||
|
||||
@@ -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.')
|
||||
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 89
|
||||
PATCH_VERSION = '0b1'
|
||||
PATCH_VERSION = '2'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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==20190228.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
|
||||
@@ -1255,10 +1256,10 @@ pysher==1.0.1
|
||||
pysma==0.3.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartapp==0.3.0
|
||||
pysmartapp==0.3.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==0.6.3
|
||||
pysmartthings==0.6.7
|
||||
|
||||
# homeassistant.components.device_tracker.snmp
|
||||
# homeassistant.components.sensor.snmp
|
||||
@@ -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
|
||||
|
||||
@@ -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==20190228.0
|
||||
home-assistant-frontend==20190305.1
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
homekit==0.12.2
|
||||
@@ -220,13 +224,13 @@ pyps4-homeassistant==0.3.0
|
||||
pyqwikswitch==0.8
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartapp==0.3.0
|
||||
pysmartapp==0.3.1
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==0.6.3
|
||||
pysmartthings==0.6.7
|
||||
|
||||
# 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
|
||||
|
||||
@@ -108,6 +108,7 @@ TEST_REQUIREMENTS = (
|
||||
'pyupnp-async',
|
||||
'pywebpush',
|
||||
'pyHS100',
|
||||
'PyNaCl',
|
||||
'regenmaschine',
|
||||
'restrictedpython',
|
||||
'rflink',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -110,8 +110,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_midnight_turnover_after_midnight_inside_period(self):
|
||||
"""Test midnight turnover setting before midnight inside period ."""
|
||||
test_time = datetime(
|
||||
2019, 1, 10, 21, 00, 0, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 10, 21, 0, 0)).astimezone(pytz.UTC)
|
||||
config = {
|
||||
'binary_sensor': [
|
||||
{
|
||||
@@ -143,8 +143,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_midnight_turnover_before_midnight_outside_period(self):
|
||||
"""Test midnight turnover setting before midnight outside period."""
|
||||
test_time = datetime(
|
||||
2019, 1, 10, 20, 30, 0, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 10, 20, 30, 0)).astimezone(pytz.UTC)
|
||||
config = {
|
||||
'binary_sensor': [
|
||||
{
|
||||
@@ -165,8 +165,9 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_midnight_turnover_after_midnight_outside_period(self):
|
||||
"""Test midnight turnover setting before midnight inside period ."""
|
||||
test_time = datetime(
|
||||
2019, 1, 10, 20, 0, 0, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 10, 20, 0, 0)).astimezone(pytz.UTC)
|
||||
|
||||
config = {
|
||||
'binary_sensor': [
|
||||
{
|
||||
@@ -185,8 +186,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
state = self.hass.states.get('binary_sensor.night')
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
switchover_time = datetime(
|
||||
2019, 1, 11, 4, 59, 0, tzinfo=self.hass.config.time_zone)
|
||||
switchover_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 11, 4, 59, 0)).astimezone(pytz.UTC)
|
||||
with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow',
|
||||
return_value=switchover_time):
|
||||
|
||||
@@ -210,8 +211,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_from_sunrise_to_sunset(self):
|
||||
"""Test period from sunrise to sunset."""
|
||||
test_time = datetime(
|
||||
2019, 1, 12, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 12)).astimezone(pytz.UTC)
|
||||
sunrise = dt_util.as_local(get_astral_event_date(
|
||||
self.hass, 'sunrise', dt_util.as_utc(test_time)))
|
||||
sunset = dt_util.as_local(get_astral_event_date(
|
||||
@@ -299,8 +300,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_from_sunset_to_sunrise(self):
|
||||
"""Test period from sunset to sunrise."""
|
||||
test_time = datetime(
|
||||
2019, 1, 12, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 12)).astimezone(pytz.UTC)
|
||||
sunset = dt_util.as_local(get_astral_event_date(
|
||||
self.hass, 'sunset', test_time))
|
||||
sunrise = dt_util.as_local(get_astral_event_next(
|
||||
@@ -385,14 +386,14 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_offset(self):
|
||||
"""Test offset."""
|
||||
after = datetime(
|
||||
2019, 1, 10, 18, 0, 0,
|
||||
tzinfo=self.hass.config.time_zone) + \
|
||||
after = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 10, 18, 0, 0)).astimezone(pytz.UTC) + \
|
||||
timedelta(hours=1, minutes=34)
|
||||
before = datetime(
|
||||
2019, 1, 10, 22, 0, 0,
|
||||
tzinfo=self.hass.config.time_zone) + \
|
||||
|
||||
before = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 10, 22, 0, 0)).astimezone(pytz.UTC) + \
|
||||
timedelta(hours=1, minutes=45)
|
||||
|
||||
entity_id = 'binary_sensor.evening'
|
||||
config = {
|
||||
'binary_sensor': [
|
||||
@@ -457,9 +458,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_offset_overnight(self):
|
||||
"""Test offset overnight."""
|
||||
after = datetime(
|
||||
2019, 1, 10, 18, 0, 0,
|
||||
tzinfo=self.hass.config.time_zone) + \
|
||||
after = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 10, 18, 0, 0)).astimezone(pytz.UTC) + \
|
||||
timedelta(hours=1, minutes=34)
|
||||
entity_id = 'binary_sensor.evening'
|
||||
config = {
|
||||
@@ -498,7 +498,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
self.hass.config.latitude = 69.6
|
||||
self.hass.config.longitude = 18.8
|
||||
|
||||
test_time = datetime(2010, 1, 1, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2010, 1, 1)).astimezone(pytz.UTC)
|
||||
sunrise = dt_util.as_local(get_astral_event_next(
|
||||
self.hass, 'sunrise', dt_util.as_utc(test_time)))
|
||||
sunset = dt_util.as_local(get_astral_event_next(
|
||||
@@ -600,13 +601,13 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
self.hass.config.latitude = 69.6
|
||||
self.hass.config.longitude = 18.8
|
||||
|
||||
test_time = datetime(2010, 6, 1, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2010, 6, 1)).astimezone(pytz.UTC)
|
||||
|
||||
sunrise = dt_util.as_local(get_astral_event_next(
|
||||
self.hass, 'sunrise', dt_util.as_utc(test_time)))
|
||||
sunset = dt_util.as_local(get_astral_event_next(
|
||||
self.hass, 'sunset', dt_util.as_utc(test_time)))
|
||||
print(sunrise)
|
||||
print(sunset)
|
||||
config = {
|
||||
'binary_sensor': [
|
||||
{
|
||||
@@ -701,8 +702,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
|
||||
def test_sun_offset(self):
|
||||
"""Test sun event with offset."""
|
||||
test_time = datetime(
|
||||
2019, 1, 12, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 1, 12)).astimezone(pytz.UTC)
|
||||
sunrise = dt_util.as_local(get_astral_event_date(
|
||||
self.hass, 'sunrise', dt_util.as_utc(test_time)) +
|
||||
timedelta(hours=-1, minutes=-30))
|
||||
@@ -810,8 +811,8 @@ class TestBinarySensorTod(unittest.TestCase):
|
||||
def test_dst(self):
|
||||
"""Test sun event with offset."""
|
||||
self.hass.config.time_zone = pytz.timezone('CET')
|
||||
test_time = datetime(
|
||||
2019, 3, 30, 3, 0, 0, tzinfo=self.hass.config.time_zone)
|
||||
test_time = self.hass.config.time_zone.localize(
|
||||
datetime(2019, 3, 30, 3, 0, 0)).astimezone(pytz.UTC)
|
||||
config = {
|
||||
'binary_sensor': [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
1
tests/components/mobile_app/__init__.py
Normal file
1
tests/components/mobile_app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for mobile_app component."""
|
||||
275
tests/components/mobile_app/test_init.py
Normal file
275
tests/components/mobile_app/test_init.py
Normal 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'}
|
||||
@@ -326,7 +326,7 @@ def scene_fixture(scene_factory):
|
||||
def event_factory_fixture():
|
||||
"""Fixture for creating mock devices."""
|
||||
def _factory(device_id, event_type="DEVICE_EVENT", capability='',
|
||||
attribute='Updated', value='Value'):
|
||||
attribute='Updated', value='Value', data=None):
|
||||
event = Mock()
|
||||
event.event_type = event_type
|
||||
event.device_id = device_id
|
||||
@@ -334,6 +334,7 @@ def event_factory_fixture():
|
||||
event.capability = capability
|
||||
event.attribute = attribute
|
||||
event.value = value
|
||||
event.data = data
|
||||
event.location_id = str(uuid4())
|
||||
return event
|
||||
return _factory
|
||||
|
||||
@@ -235,16 +235,21 @@ async def test_broker_regenerates_token(
|
||||
|
||||
|
||||
async def test_event_handler_dispatches_updated_devices(
|
||||
hass, config_entry, device_factory, event_request_factory):
|
||||
hass, config_entry, device_factory, event_request_factory,
|
||||
event_factory):
|
||||
"""Test the event handler dispatches updated devices."""
|
||||
devices = [
|
||||
device_factory('Bedroom 1 Switch', ['switch']),
|
||||
device_factory('Bathroom 1', ['switch']),
|
||||
device_factory('Sensor', ['motionSensor']),
|
||||
device_factory('Lock', ['lock'])
|
||||
]
|
||||
device_ids = [devices[0].device_id, devices[1].device_id,
|
||||
devices[2].device_id]
|
||||
request = event_request_factory(device_ids)
|
||||
devices[2].device_id, devices[3].device_id]
|
||||
event = event_factory(devices[3].device_id, capability='lock',
|
||||
attribute='lock', value='locked',
|
||||
data={'codeId': '1'})
|
||||
request = event_request_factory(device_ids=device_ids, events=[event])
|
||||
config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id
|
||||
called = False
|
||||
|
||||
@@ -265,6 +270,8 @@ async def test_event_handler_dispatches_updated_devices(
|
||||
assert called
|
||||
for device in devices:
|
||||
assert device.status.values['Updated'] == 'Value'
|
||||
assert devices[3].status.attributes['lock'].value == 'locked'
|
||||
assert devices[3].status.attributes['lock'].data == {'codeId': '1'}
|
||||
|
||||
|
||||
async def test_event_handler_ignores_other_installed_app(
|
||||
@@ -308,7 +315,8 @@ async def test_event_handler_fires_button_events(
|
||||
'device_id': device.device_id,
|
||||
'location_id': event.location_id,
|
||||
'value': 'pushed',
|
||||
'name': device.label
|
||||
'name': device.label,
|
||||
'data': None
|
||||
}
|
||||
hass.bus.async_listen(EVENT_BUTTON, handler)
|
||||
broker = smartthings.DeviceBroker(
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user