commit
3701c0f219
homeassistant
auth
providers
components
binary_sensor
emulated_hue
google_assistant
group
owntracks
tests
|
@ -462,10 +462,11 @@ class AuthStore:
|
|||
for group in self._groups.values():
|
||||
g_dict = {
|
||||
'id': group.id,
|
||||
# Name not read for sys groups. Kept here for backwards compat
|
||||
'name': group.name
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if group.id not in (GROUP_ID_READ_ONLY, GROUP_ID_ADMIN):
|
||||
g_dict['name'] = group.name
|
||||
g_dict['policy'] = group.policy
|
||||
|
||||
groups.append(g_dict)
|
||||
|
|
|
@ -4,16 +4,19 @@ Support Legacy API password auth provider.
|
|||
It will be removed when auth system production ready
|
||||
"""
|
||||
import hmac
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from typing import Any, Dict, Optional, cast, TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
from .. import AuthManager
|
||||
from ..models import Credentials, UserMeta, User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
|
@ -31,6 +34,24 @@ class InvalidAuthError(HomeAssistantError):
|
|||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
async def async_get_user(hass: HomeAssistant) -> User:
|
||||
"""Return the legacy API password user."""
|
||||
auth = cast(AuthManager, hass.auth) # type: ignore
|
||||
found = None
|
||||
|
||||
for prv in auth.auth_providers:
|
||||
if prv.type == 'legacy_api_password':
|
||||
found = prv
|
||||
break
|
||||
|
||||
if found is None:
|
||||
raise ValueError('Legacy API password provider not found')
|
||||
|
||||
return await auth.async_get_or_create_user(
|
||||
await found.async_get_or_create_credentials({})
|
||||
)
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
|
|
@ -74,7 +74,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
|
|||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def update(self):
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
|
|
|
@ -97,8 +97,8 @@ async def async_setup(hass, yaml_config):
|
|||
app._on_startup.freeze()
|
||||
await app.startup()
|
||||
|
||||
handler = None
|
||||
server = None
|
||||
runner = None
|
||||
site = None
|
||||
|
||||
DescriptionXmlView(config).register(app, app.router)
|
||||
HueUsernameView().register(app, app.router)
|
||||
|
@ -115,25 +115,24 @@ async def async_setup(hass, yaml_config):
|
|||
async def stop_emulated_hue_bridge(event):
|
||||
"""Stop the emulated hue bridge."""
|
||||
upnp_listener.stop()
|
||||
if server:
|
||||
server.close()
|
||||
await server.wait_closed()
|
||||
await app.shutdown()
|
||||
if handler:
|
||||
await handler.shutdown(10)
|
||||
await app.cleanup()
|
||||
if site:
|
||||
await site.stop()
|
||||
if runner:
|
||||
await runner.cleanup()
|
||||
|
||||
async def start_emulated_hue_bridge(event):
|
||||
"""Start the emulated hue bridge."""
|
||||
upnp_listener.start()
|
||||
nonlocal handler
|
||||
nonlocal server
|
||||
nonlocal site
|
||||
nonlocal runner
|
||||
|
||||
handler = app.make_handler(loop=hass.loop)
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
|
||||
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
|
||||
|
||||
try:
|
||||
server = await hass.loop.create_server(
|
||||
handler, config.host_ip_addr, config.listen_port)
|
||||
await site.start()
|
||||
except OSError as error:
|
||||
_LOGGER.error("Failed to create HTTP server at port %d: %s",
|
||||
config.listen_port, error)
|
||||
|
|
|
@ -103,29 +103,31 @@ class FibaroController():
|
|||
"""Handle change report received from the HomeCenter."""
|
||||
callback_set = set()
|
||||
for change in state.get('changes', []):
|
||||
dev_id = change.pop('id')
|
||||
for property_name, value in change.items():
|
||||
if property_name == 'log':
|
||||
if value and value != "transfer OK":
|
||||
_LOGGER.debug("LOG %s: %s",
|
||||
self._device_map[dev_id].friendly_name,
|
||||
value)
|
||||
try:
|
||||
dev_id = change.pop('id')
|
||||
if dev_id not in self._device_map.keys():
|
||||
continue
|
||||
if property_name == 'logTemp':
|
||||
continue
|
||||
if property_name in self._device_map[dev_id].properties:
|
||||
self._device_map[dev_id].properties[property_name] = \
|
||||
value
|
||||
_LOGGER.debug("<- %s.%s = %s",
|
||||
self._device_map[dev_id].ha_id,
|
||||
property_name,
|
||||
str(value))
|
||||
else:
|
||||
_LOGGER.warning("Error updating %s data of %s, not found",
|
||||
property_name,
|
||||
self._device_map[dev_id].ha_id)
|
||||
if dev_id in self._callbacks:
|
||||
callback_set.add(dev_id)
|
||||
device = self._device_map[dev_id]
|
||||
for property_name, value in change.items():
|
||||
if property_name == 'log':
|
||||
if value and value != "transfer OK":
|
||||
_LOGGER.debug("LOG %s: %s",
|
||||
device.friendly_name, value)
|
||||
continue
|
||||
if property_name == 'logTemp':
|
||||
continue
|
||||
if property_name in device.properties:
|
||||
device.properties[property_name] = \
|
||||
value
|
||||
_LOGGER.debug("<- %s.%s = %s", device.ha_id,
|
||||
property_name, str(value))
|
||||
else:
|
||||
_LOGGER.warning("%s.%s not found", device.ha_id,
|
||||
property_name)
|
||||
if dev_id in self._callbacks:
|
||||
callback_set.add(dev_id)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
for item in callback_set:
|
||||
self._callbacks[item]()
|
||||
|
||||
|
@ -137,8 +139,12 @@ class FibaroController():
|
|||
def _map_device_to_type(device):
|
||||
"""Map device to HA device type."""
|
||||
# Use our lookup table to identify device type
|
||||
device_type = FIBARO_TYPEMAP.get(
|
||||
device.type, FIBARO_TYPEMAP.get(device.baseType))
|
||||
if 'type' in device:
|
||||
device_type = FIBARO_TYPEMAP.get(device.type)
|
||||
elif 'baseType' in device:
|
||||
device_type = FIBARO_TYPEMAP.get(device.baseType)
|
||||
else:
|
||||
device_type = None
|
||||
|
||||
# We can also identify device type by its capabilities
|
||||
if device_type is None:
|
||||
|
@ -156,8 +162,7 @@ class FibaroController():
|
|||
|
||||
# Switches that control lights should show up as lights
|
||||
if device_type == 'switch' and \
|
||||
'isLight' in device.properties and \
|
||||
device.properties.isLight == 'true':
|
||||
device.properties.get('isLight', 'false') == 'true':
|
||||
device_type = 'light'
|
||||
return device_type
|
||||
|
||||
|
@ -165,26 +170,31 @@ class FibaroController():
|
|||
"""Read and process the device list."""
|
||||
devices = self._client.devices.list()
|
||||
self._device_map = {}
|
||||
for device in devices:
|
||||
if device.roomID == 0:
|
||||
room_name = 'Unknown'
|
||||
else:
|
||||
room_name = self._room_map[device.roomID].name
|
||||
device.friendly_name = room_name + ' ' + device.name
|
||||
device.ha_id = '{}_{}_{}'.format(
|
||||
slugify(room_name), slugify(device.name), device.id)
|
||||
self._device_map[device.id] = device
|
||||
self.fibaro_devices = defaultdict(list)
|
||||
for device in self._device_map.values():
|
||||
if device.enabled and \
|
||||
(not device.isPlugin or self._import_plugins):
|
||||
device.mapped_type = self._map_device_to_type(device)
|
||||
for device in devices:
|
||||
try:
|
||||
if device.roomID == 0:
|
||||
room_name = 'Unknown'
|
||||
else:
|
||||
room_name = self._room_map[device.roomID].name
|
||||
device.friendly_name = room_name + ' ' + device.name
|
||||
device.ha_id = '{}_{}_{}'.format(
|
||||
slugify(room_name), slugify(device.name), device.id)
|
||||
if device.enabled and \
|
||||
('isPlugin' not in device or
|
||||
(not device.isPlugin or self._import_plugins)):
|
||||
device.mapped_type = self._map_device_to_type(device)
|
||||
else:
|
||||
device.mapped_type = None
|
||||
if device.mapped_type:
|
||||
self._device_map[device.id] = device
|
||||
self.fibaro_devices[device.mapped_type].append(device)
|
||||
else:
|
||||
_LOGGER.debug("%s (%s, %s) not mapped",
|
||||
_LOGGER.debug("%s (%s, %s) not used",
|
||||
device.ha_id, device.type,
|
||||
device.baseType)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
|
@ -712,6 +712,8 @@ class FanSpeedTrait(_Trait):
|
|||
modes = self.state.attributes.get(fan.ATTR_SPEED_LIST, [])
|
||||
speeds = []
|
||||
for mode in modes:
|
||||
if mode not in self.speed_synonyms:
|
||||
continue
|
||||
speed = {
|
||||
"speed_name": mode,
|
||||
"speed_values": [{
|
||||
|
|
|
@ -207,6 +207,13 @@ async def async_setup(hass, config):
|
|||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
service_lock = asyncio.Lock()
|
||||
|
||||
async def locked_service_handler(service):
|
||||
"""Handle a service with an async lock."""
|
||||
async with service_lock:
|
||||
await groups_service_handler(service)
|
||||
|
||||
async def groups_service_handler(service):
|
||||
"""Handle dynamic group service functions."""
|
||||
object_id = service.data[ATTR_OBJECT_ID]
|
||||
|
@ -284,7 +291,7 @@ async def async_setup(hass, config):
|
|||
await component.async_remove_entity(entity_id)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET, groups_service_handler,
|
||||
DOMAIN, SERVICE_SET, locked_service_handler,
|
||||
schema=SET_SERVICE_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
|
|
|
@ -302,12 +302,6 @@ class HomeAssistantHTTP:
|
|||
|
||||
async def start(self):
|
||||
"""Start the aiohttp server."""
|
||||
# We misunderstood the startup signal. You're not allowed to change
|
||||
# anything during startup. Temp workaround.
|
||||
# pylint: disable=protected-access
|
||||
self.app._on_startup.freeze()
|
||||
await self.app.startup()
|
||||
|
||||
if self.ssl_certificate:
|
||||
try:
|
||||
if self.ssl_profile == SSL_INTERMEDIATE:
|
||||
|
@ -335,6 +329,7 @@ class HomeAssistantHTTP:
|
|||
# However in Home Assistant components can be discovered after boot.
|
||||
# This will now raise a RunTimeError.
|
||||
# To work around this we now prevent the router from getting frozen
|
||||
# pylint: disable=protected-access
|
||||
self.app._router.freeze = lambda: None
|
||||
|
||||
self.runner = web.AppRunner(self.app)
|
||||
|
|
|
@ -10,6 +10,7 @@ import jwt
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||
from homeassistant.auth.providers import legacy_api_password
|
||||
from homeassistant.auth.util import generate_secret
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
@ -78,12 +79,16 @@ def setup_auth(app, trusted_networks, use_auth,
|
|||
request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
request['hass_user'] = await legacy_api_password.async_get_user(
|
||||
app['hass'])
|
||||
|
||||
elif (legacy_auth and DATA_API_PASSWORD in request.query and
|
||||
hmac.compare_digest(
|
||||
api_password.encode('utf-8'),
|
||||
request.query[DATA_API_PASSWORD].encode('utf-8'))):
|
||||
authenticated = True
|
||||
request['hass_user'] = await legacy_api_password.async_get_user(
|
||||
app['hass'])
|
||||
|
||||
elif _is_trusted_ip(request, trusted_networks):
|
||||
authenticated = True
|
||||
|
@ -96,11 +101,7 @@ def setup_auth(app, trusted_networks, use_auth,
|
|||
request[KEY_AUTHENTICATED] = authenticated
|
||||
return await handler(request)
|
||||
|
||||
async def auth_startup(app):
|
||||
"""Initialize auth middleware when app starts up."""
|
||||
app.middlewares.append(auth_middleware)
|
||||
|
||||
app.on_startup.append(auth_startup)
|
||||
app.middlewares.append(auth_middleware)
|
||||
|
||||
|
||||
def _is_trusted_ip(request, trusted_networks):
|
||||
|
|
|
@ -9,7 +9,7 @@ from aiohttp.web import middleware
|
|||
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -36,13 +36,14 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
|
|||
@callback
|
||||
def setup_bans(hass, app, login_threshold):
|
||||
"""Create IP Ban middleware for the app."""
|
||||
app.middlewares.append(ban_middleware)
|
||||
app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
|
||||
app[KEY_LOGIN_THRESHOLD] = login_threshold
|
||||
|
||||
async def ban_startup(app):
|
||||
"""Initialize bans when app starts up."""
|
||||
app.middlewares.append(ban_middleware)
|
||||
app[KEY_BANNED_IPS] = await hass.async_add_job(
|
||||
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||
app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
|
||||
app[KEY_LOGIN_THRESHOLD] = login_threshold
|
||||
app[KEY_BANNED_IPS] = await async_load_ip_bans_config(
|
||||
hass, hass.config.path(IP_BANS_FILE))
|
||||
|
||||
app.on_startup.append(ban_startup)
|
||||
|
||||
|
@ -149,7 +150,7 @@ class IpBan:
|
|||
self.banned_at = banned_at or datetime.utcnow()
|
||||
|
||||
|
||||
def load_ip_bans_config(path: str):
|
||||
async def async_load_ip_bans_config(hass: HomeAssistant, path: str):
|
||||
"""Load list of banned IPs from config file."""
|
||||
ip_list = []
|
||||
|
||||
|
@ -157,7 +158,7 @@ def load_ip_bans_config(path: str):
|
|||
return ip_list
|
||||
|
||||
try:
|
||||
list_ = load_yaml_config_file(path)
|
||||
list_ = await hass.async_add_executor_job(load_yaml_config_file, path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return ip_list
|
||||
|
|
|
@ -33,8 +33,4 @@ def setup_real_ip(app, use_x_forwarded_for, trusted_proxies):
|
|||
|
||||
return await handler(request)
|
||||
|
||||
async def app_startup(app):
|
||||
"""Initialize bans when app starts up."""
|
||||
app.middlewares.append(real_ip_middleware)
|
||||
|
||||
app.on_startup.append(app_startup)
|
||||
app.middlewares.append(real_ip_middleware)
|
||||
|
|
|
@ -445,6 +445,12 @@ def _exclude_events(events, entities_filter):
|
|||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
elif event.event_type == EVENT_ALEXA_SMART_HOME:
|
||||
domain = 'alexa'
|
||||
|
||||
elif event.event_type == EVENT_HOMEKIT_CHANGED:
|
||||
domain = DOMAIN_HOMEKIT
|
||||
|
||||
if not entity_id and domain:
|
||||
entity_id = "%s." % (domain, )
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@ class OwnTracksFlow(config_entries.ConfigFlow):
|
|||
|
||||
if supports_encryption():
|
||||
secret_desc = (
|
||||
"The encryption key is {secret} "
|
||||
"(on Android under preferences -> advanced)")
|
||||
"The encryption key is {} "
|
||||
"(on Android under preferences -> advanced)".format(secret))
|
||||
else:
|
||||
secret_desc = (
|
||||
"Encryption is not supported because libsodium is not "
|
||||
|
|
|
@ -77,7 +77,7 @@ class RainMachineSensor(RainMachineEntity):
|
|||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def update(self):
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle, slugify
|
||||
|
||||
REQUIREMENTS = ['py17track==2.0.2']
|
||||
REQUIREMENTS = ['py17track==2.1.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DESTINATION_COUNTRY = 'destination_country'
|
||||
|
|
|
@ -38,12 +38,28 @@ SERVICE_ITEM_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
WS_TYPE_SHOPPING_LIST_ITEMS = 'shopping_list/items'
|
||||
WS_TYPE_SHOPPING_LIST_ADD_ITEM = 'shopping_list/items/add'
|
||||
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM = 'shopping_list/items/update'
|
||||
|
||||
SCHEMA_WEBSOCKET_ITEMS = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SHOPPING_LIST_ITEMS
|
||||
})
|
||||
|
||||
SCHEMA_WEBSOCKET_ADD_ITEM = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SHOPPING_LIST_ADD_ITEM,
|
||||
vol.Required('name'): str
|
||||
})
|
||||
|
||||
SCHEMA_WEBSOCKET_UPDATE_ITEM = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
||||
vol.Required('item_id'): str,
|
||||
vol.Optional('name'): str,
|
||||
vol.Optional('complete'): bool
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
|
@ -103,6 +119,14 @@ def async_setup(hass, config):
|
|||
WS_TYPE_SHOPPING_LIST_ITEMS,
|
||||
websocket_handle_items,
|
||||
SCHEMA_WEBSOCKET_ITEMS)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_SHOPPING_LIST_ADD_ITEM,
|
||||
websocket_handle_add,
|
||||
SCHEMA_WEBSOCKET_ADD_ITEM)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
||||
websocket_handle_update,
|
||||
SCHEMA_WEBSOCKET_UPDATE_ITEM)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -276,3 +300,30 @@ def websocket_handle_items(hass, connection, msg):
|
|||
"""Handle get shopping_list items."""
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], hass.data[DOMAIN].items))
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_handle_add(hass, connection, msg):
|
||||
"""Handle add item to shopping_list."""
|
||||
item = hass.data[DOMAIN].async_add(msg['name'])
|
||||
hass.bus.async_fire(EVENT)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], item))
|
||||
|
||||
|
||||
@websocket_api.async_response
|
||||
async def websocket_handle_update(hass, connection, msg):
|
||||
"""Handle update shopping_list item."""
|
||||
msg_id = msg.pop('id')
|
||||
item_id = msg.pop('item_id')
|
||||
msg.pop('type')
|
||||
data = msg
|
||||
|
||||
try:
|
||||
item = hass.data[DOMAIN].async_update(item_id, data)
|
||||
hass.bus.async_fire(EVENT)
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg_id, item))
|
||||
except KeyError:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg_id, 'item_not_found', 'Item not found'))
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 83
|
||||
PATCH_VERSION = '0'
|
||||
PATCH_VERSION = '1'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
|
|
@ -804,7 +804,7 @@ py-melissa-climate==2.0.0
|
|||
py-synology==0.2.0
|
||||
|
||||
# homeassistant.components.sensor.seventeentrack
|
||||
py17track==2.0.2
|
||||
py17track==2.1.0
|
||||
|
||||
# homeassistant.components.hdmi_cec
|
||||
pyCEC==0.4.13
|
||||
|
|
|
@ -199,13 +199,22 @@ async def test_loading_empty_data(hass, hass_storage):
|
|||
assert len(users) == 0
|
||||
|
||||
|
||||
async def test_system_groups_only_store_id(hass, hass_storage):
|
||||
"""Test that for system groups we only store the ID."""
|
||||
async def test_system_groups_store_id_and_name(hass, hass_storage):
|
||||
"""Test that for system groups we store the ID and name.
|
||||
|
||||
Name is stored so that we remain backwards compat with < 0.82.
|
||||
"""
|
||||
store = auth_store.AuthStore(hass)
|
||||
await store._async_load()
|
||||
data = store._data_to_save()
|
||||
assert len(data['users']) == 0
|
||||
assert data['groups'] == [
|
||||
{'id': auth_store.GROUP_ID_ADMIN},
|
||||
{'id': auth_store.GROUP_ID_READ_ONLY},
|
||||
{
|
||||
'id': auth_store.GROUP_ID_ADMIN,
|
||||
'name': auth_store.GROUP_NAME_ADMIN,
|
||||
},
|
||||
{
|
||||
'id': auth_store.GROUP_ID_READ_ONLY,
|
||||
'name': auth_store.GROUP_NAME_READ_ONLY,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -23,7 +23,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3"
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def alexa_client(loop, hass, aiohttp_client):
|
||||
def alexa_client(loop, hass, hass_client):
|
||||
"""Initialize a Home Assistant server for testing this module."""
|
||||
@callback
|
||||
def mock_service(call):
|
||||
|
@ -95,7 +95,7 @@ def alexa_client(loop, hass, aiohttp_client):
|
|||
},
|
||||
}
|
||||
}))
|
||||
return loop.run_until_complete(aiohttp_client(hass.http.app))
|
||||
return loop.run_until_complete(hass_client())
|
||||
|
||||
|
||||
def _intent_req(client, data=None):
|
||||
|
|
|
@ -1437,10 +1437,10 @@ async def test_unsupported_domain(hass):
|
|||
assert not msg['payload']['endpoints']
|
||||
|
||||
|
||||
async def do_http_discovery(config, hass, aiohttp_client):
|
||||
async def do_http_discovery(config, hass, hass_client):
|
||||
"""Submit a request to the Smart Home HTTP API."""
|
||||
await async_setup_component(hass, alexa.DOMAIN, config)
|
||||
http_client = await aiohttp_client(hass.http.app)
|
||||
http_client = await hass_client()
|
||||
|
||||
request = get_new_request('Alexa.Discovery', 'Discover')
|
||||
response = await http_client.post(
|
||||
|
@ -1450,7 +1450,7 @@ async def do_http_discovery(config, hass, aiohttp_client):
|
|||
return response
|
||||
|
||||
|
||||
async def test_http_api(hass, aiohttp_client):
|
||||
async def test_http_api(hass, hass_client):
|
||||
"""With `smart_home:` HTTP API is exposed."""
|
||||
config = {
|
||||
'alexa': {
|
||||
|
@ -1458,7 +1458,7 @@ async def test_http_api(hass, aiohttp_client):
|
|||
}
|
||||
}
|
||||
|
||||
response = await do_http_discovery(config, hass, aiohttp_client)
|
||||
response = await do_http_discovery(config, hass, hass_client)
|
||||
response_data = await response.json()
|
||||
|
||||
# Here we're testing just the HTTP view glue -- details of discovery are
|
||||
|
@ -1466,12 +1466,12 @@ async def test_http_api(hass, aiohttp_client):
|
|||
assert response_data['event']['header']['name'] == 'Discover.Response'
|
||||
|
||||
|
||||
async def test_http_api_disabled(hass, aiohttp_client):
|
||||
async def test_http_api_disabled(hass, hass_client):
|
||||
"""Without `smart_home:`, the HTTP API is disabled."""
|
||||
config = {
|
||||
'alexa': {}
|
||||
}
|
||||
response = await do_http_discovery(config, hass, aiohttp_client)
|
||||
response = await do_http_discovery(config, hass, hass_client)
|
||||
|
||||
assert response.status == 404
|
||||
|
||||
|
|
|
@ -4,12 +4,21 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
||||
from homeassistant.auth.providers import legacy_api_password, homeassistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components.websocket_api.http import URL
|
||||
from homeassistant.components.websocket_api.auth import (
|
||||
TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED)
|
||||
|
||||
from tests.common import MockUser, CLIENT_ID
|
||||
from tests.common import MockUser, CLIENT_ID, mock_coro
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def prevent_io():
|
||||
"""Fixture to prevent certain I/O from happening."""
|
||||
with patch('homeassistant.components.http.ban.async_load_ip_bans_config',
|
||||
side_effect=lambda *args: mock_coro([])):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -80,7 +89,7 @@ def hass_access_token(hass, hass_admin_user):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def hass_admin_user(hass):
|
||||
def hass_admin_user(hass, local_auth):
|
||||
"""Return a Home Assistant admin user."""
|
||||
admin_group = hass.loop.run_until_complete(hass.auth.async_get_group(
|
||||
GROUP_ID_ADMIN))
|
||||
|
@ -88,8 +97,42 @@ def hass_admin_user(hass):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def hass_read_only_user(hass):
|
||||
def hass_read_only_user(hass, local_auth):
|
||||
"""Return a Home Assistant read only user."""
|
||||
read_only_group = hass.loop.run_until_complete(hass.auth.async_get_group(
|
||||
GROUP_ID_READ_ONLY))
|
||||
return MockUser(groups=[read_only_group]).add_to_hass(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_auth(hass):
|
||||
"""Load legacy API password provider."""
|
||||
prv = legacy_api_password.LegacyApiPasswordAuthProvider(
|
||||
hass, hass.auth._store, {
|
||||
'type': 'legacy_api_password'
|
||||
}
|
||||
)
|
||||
hass.auth._providers[(prv.type, prv.id)] = prv
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def local_auth(hass):
|
||||
"""Load local auth provider."""
|
||||
prv = homeassistant.HassAuthProvider(
|
||||
hass, hass.auth._store, {
|
||||
'type': 'homeassistant'
|
||||
}
|
||||
)
|
||||
hass.auth._providers[(prv.type, prv.id)] = prv
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hass_client(hass, aiohttp_client, hass_access_token):
|
||||
"""Return an authenticated HTTP client."""
|
||||
async def auth_client():
|
||||
"""Return an authenticated client."""
|
||||
return await aiohttp_client(hass.http.app, headers={
|
||||
'Authorization': "Bearer {}".format(hass_access_token)
|
||||
})
|
||||
|
||||
return auth_client
|
||||
|
|
|
@ -27,7 +27,7 @@ def hassio_env():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def hassio_client(hassio_env, hass, aiohttp_client):
|
||||
def hassio_client(hassio_env, hass, aiohttp_client, legacy_auth):
|
||||
"""Create mock hassio http client."""
|
||||
with patch('homeassistant.components.hassio.HassIO.update_hass_api',
|
||||
Mock(return_value=mock_coro({"result": "ok"}))), \
|
||||
|
|
|
@ -83,7 +83,8 @@ async def test_access_without_password(app, aiohttp_client):
|
|||
assert resp.status == 200
|
||||
|
||||
|
||||
async def test_access_with_password_in_header(app, aiohttp_client):
|
||||
async def test_access_with_password_in_header(app, aiohttp_client,
|
||||
legacy_auth):
|
||||
"""Test access with password in header."""
|
||||
setup_auth(app, [], False, api_password=API_PASSWORD)
|
||||
client = await aiohttp_client(app)
|
||||
|
@ -97,7 +98,7 @@ async def test_access_with_password_in_header(app, aiohttp_client):
|
|||
assert req.status == 401
|
||||
|
||||
|
||||
async def test_access_with_password_in_query(app, aiohttp_client):
|
||||
async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth):
|
||||
"""Test access with password in URL."""
|
||||
setup_auth(app, [], False, api_password=API_PASSWORD)
|
||||
client = await aiohttp_client(app)
|
||||
|
@ -219,7 +220,8 @@ async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client):
|
|||
"{} should be trusted".format(remote_addr)
|
||||
|
||||
|
||||
async def test_auth_active_blocked_api_password_access(app, aiohttp_client):
|
||||
async def test_auth_active_blocked_api_password_access(
|
||||
app, aiohttp_client, legacy_auth):
|
||||
"""Test access using api_password should be blocked when auth.active."""
|
||||
setup_auth(app, [], True, api_password=API_PASSWORD)
|
||||
client = await aiohttp_client(app)
|
||||
|
@ -239,7 +241,8 @@ async def test_auth_active_blocked_api_password_access(app, aiohttp_client):
|
|||
assert req.status == 401
|
||||
|
||||
|
||||
async def test_auth_legacy_support_api_password_access(app, aiohttp_client):
|
||||
async def test_auth_legacy_support_api_password_access(
|
||||
app, aiohttp_client, legacy_auth):
|
||||
"""Test access using api_password if auth.support_legacy."""
|
||||
setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD)
|
||||
client = await aiohttp_client(app)
|
||||
|
|
|
@ -16,6 +16,9 @@ from homeassistant.components.http.ban import (
|
|||
|
||||
from . import mock_real_ip
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
||||
BANNED_IPS = ['200.201.202.203', '100.64.0.2']
|
||||
|
||||
|
||||
|
@ -25,9 +28,9 @@ async def test_access_from_banned_ip(hass, aiohttp_client):
|
|||
setup_bans(hass, app, 5)
|
||||
set_real_ip = mock_real_ip(app)
|
||||
|
||||
with patch('homeassistant.components.http.ban.load_ip_bans_config',
|
||||
return_value=[IpBan(banned_ip) for banned_ip
|
||||
in BANNED_IPS]):
|
||||
with patch('homeassistant.components.http.ban.async_load_ip_bans_config',
|
||||
return_value=mock_coro([IpBan(banned_ip) for banned_ip
|
||||
in BANNED_IPS])):
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
for remote_addr in BANNED_IPS:
|
||||
|
@ -71,9 +74,9 @@ async def test_ip_bans_file_creation(hass, aiohttp_client):
|
|||
setup_bans(hass, app, 1)
|
||||
mock_real_ip(app)("200.201.202.204")
|
||||
|
||||
with patch('homeassistant.components.http.ban.load_ip_bans_config',
|
||||
return_value=[IpBan(banned_ip) for banned_ip
|
||||
in BANNED_IPS]):
|
||||
with patch('homeassistant.components.http.ban.async_load_ip_bans_config',
|
||||
return_value=mock_coro([IpBan(banned_ip) for banned_ip
|
||||
in BANNED_IPS])):
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
m = mock_open()
|
||||
|
|
|
@ -124,7 +124,7 @@ async def test_api_no_base_url(hass):
|
|||
assert hass.config.api.base_url == 'http://127.0.0.1:8123'
|
||||
|
||||
|
||||
async def test_not_log_password(hass, aiohttp_client, caplog):
|
||||
async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth):
|
||||
"""Test access with password doesn't get logged."""
|
||||
assert await async_setup_component(hass, 'api', {
|
||||
'http': {
|
||||
|
|
|
@ -16,12 +16,10 @@ from tests.common import async_mock_service
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_client(hass, aiohttp_client, hass_access_token):
|
||||
def mock_api_client(hass, hass_client):
|
||||
"""Start the Hass HTTP component and return admin API client."""
|
||||
hass.loop.run_until_complete(async_setup_component(hass, 'api', {}))
|
||||
return hass.loop.run_until_complete(aiohttp_client(hass.http.app, headers={
|
||||
'Authorization': 'Bearer {}'.format(hass_access_token)
|
||||
}))
|
||||
return hass.loop.run_until_complete(hass_client())
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -408,7 +406,7 @@ def _listen_count(hass):
|
|||
|
||||
|
||||
async def test_api_error_log(hass, aiohttp_client, hass_access_token,
|
||||
hass_admin_user):
|
||||
hass_admin_user, legacy_auth):
|
||||
"""Test if we can fetch the error log."""
|
||||
hass.data[DATA_LOGGING] = '/some/path'
|
||||
await async_setup_component(hass, 'api', {
|
||||
|
@ -566,5 +564,17 @@ async def test_rendering_template_admin(hass, mock_api_client,
|
|||
hass_admin_user):
|
||||
"""Test rendering a template requires admin."""
|
||||
hass_admin_user.groups = []
|
||||
resp = await mock_api_client.post('/api/template')
|
||||
resp = await mock_api_client.post(const.URL_API_TEMPLATE)
|
||||
assert resp.status == 401
|
||||
|
||||
|
||||
async def test_rendering_template_legacy_user(
|
||||
hass, mock_api_client, aiohttp_client, legacy_auth):
|
||||
"""Test rendering a template with legacy API password."""
|
||||
hass.states.async_set('sensor.temperature', 10)
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
resp = await client.post(
|
||||
const.URL_API_TEMPLATE,
|
||||
json={"template": '{{ states.sensor.temperature.state }}'}
|
||||
)
|
||||
assert resp.status == 401
|
||||
|
|
|
@ -90,7 +90,7 @@ async def test_register_before_setup(hass):
|
|||
assert intent.text_input == 'I would like the Grolsch beer'
|
||||
|
||||
|
||||
async def test_http_processing_intent(hass, aiohttp_client):
|
||||
async def test_http_processing_intent(hass, hass_client):
|
||||
"""Test processing intent via HTTP API."""
|
||||
class TestIntentHandler(intent.IntentHandler):
|
||||
"""Test Intent Handler."""
|
||||
|
@ -120,7 +120,7 @@ async def test_http_processing_intent(hass, aiohttp_client):
|
|||
})
|
||||
assert result
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
client = await hass_client()
|
||||
resp = await client.post('/api/conversation/process', json={
|
||||
'text': 'I would like the Grolsch beer'
|
||||
})
|
||||
|
@ -244,7 +244,7 @@ async def test_toggle_intent(hass, sentence):
|
|||
assert call.data == {'entity_id': 'light.kitchen'}
|
||||
|
||||
|
||||
async def test_http_api(hass, aiohttp_client):
|
||||
async def test_http_api(hass, hass_client):
|
||||
"""Test the HTTP conversation API."""
|
||||
result = await component.async_setup(hass, {})
|
||||
assert result
|
||||
|
@ -252,7 +252,7 @@ async def test_http_api(hass, aiohttp_client):
|
|||
result = await async_setup_component(hass, 'conversation', {})
|
||||
assert result
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
client = await hass_client()
|
||||
hass.states.async_set('light.kitchen', 'off')
|
||||
calls = async_mock_service(hass, HASS_DOMAIN, 'turn_on')
|
||||
|
||||
|
@ -268,7 +268,7 @@ async def test_http_api(hass, aiohttp_client):
|
|||
assert call.data == {'entity_id': 'light.kitchen'}
|
||||
|
||||
|
||||
async def test_http_api_wrong_data(hass, aiohttp_client):
|
||||
async def test_http_api_wrong_data(hass, hass_client):
|
||||
"""Test the HTTP conversation API."""
|
||||
result = await component.async_setup(hass, {})
|
||||
assert result
|
||||
|
@ -276,7 +276,7 @@ async def test_http_api_wrong_data(hass, aiohttp_client):
|
|||
result = await async_setup_component(hass, 'conversation', {})
|
||||
assert result
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.post('/api/conversation/process', json={
|
||||
'text': 123
|
||||
|
|
|
@ -515,13 +515,13 @@ class TestComponentHistory(unittest.TestCase):
|
|||
return zero, four, states
|
||||
|
||||
|
||||
async def test_fetch_period_api(hass, aiohttp_client):
|
||||
async def test_fetch_period_api(hass, hass_client):
|
||||
"""Test the fetch period view for history."""
|
||||
await hass.async_add_job(init_recorder_component, hass)
|
||||
await async_setup_component(hass, 'history', {})
|
||||
await hass.components.recorder.wait_connection_ready()
|
||||
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
client = await hass_client()
|
||||
response = await client.get(
|
||||
'/api/history/period/{}'.format(dt_util.utcnow().isoformat()))
|
||||
assert response.status == 200
|
||||
|
|
|
@ -242,9 +242,11 @@ class TestComponentLogbook(unittest.TestCase):
|
|||
config = logbook.CONFIG_SCHEMA({
|
||||
ha.DOMAIN: {},
|
||||
logbook.DOMAIN: {logbook.CONF_EXCLUDE: {
|
||||
logbook.CONF_DOMAINS: ['switch', ]}}})
|
||||
logbook.CONF_DOMAINS: ['switch', 'alexa', DOMAIN_HOMEKIT]}}})
|
||||
events = logbook._exclude_events(
|
||||
(ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB),
|
||||
(ha.Event(EVENT_HOMEASSISTANT_START),
|
||||
ha.Event(EVENT_ALEXA_SMART_HOME),
|
||||
ha.Event(EVENT_HOMEKIT_CHANGED), eventA, eventB),
|
||||
logbook._generate_filter_from_config(config[logbook.DOMAIN]))
|
||||
entries = list(logbook.humanify(self.hass, events))
|
||||
|
||||
|
@ -325,22 +327,35 @@ class TestComponentLogbook(unittest.TestCase):
|
|||
pointA = dt_util.utcnow()
|
||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
||||
|
||||
event_alexa = ha.Event(EVENT_ALEXA_SMART_HOME, {'request': {
|
||||
'namespace': 'Alexa.Discovery',
|
||||
'name': 'Discover',
|
||||
}})
|
||||
event_homekit = ha.Event(EVENT_HOMEKIT_CHANGED, {
|
||||
ATTR_ENTITY_ID: 'lock.front_door',
|
||||
ATTR_DISPLAY_NAME: 'Front Door',
|
||||
ATTR_SERVICE: 'lock',
|
||||
})
|
||||
|
||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
||||
|
||||
config = logbook.CONFIG_SCHEMA({
|
||||
ha.DOMAIN: {},
|
||||
logbook.DOMAIN: {logbook.CONF_INCLUDE: {
|
||||
logbook.CONF_DOMAINS: ['sensor', ]}}})
|
||||
logbook.CONF_DOMAINS: ['sensor', 'alexa', DOMAIN_HOMEKIT]}}})
|
||||
events = logbook._exclude_events(
|
||||
(ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB),
|
||||
(ha.Event(EVENT_HOMEASSISTANT_START),
|
||||
event_alexa, event_homekit, eventA, eventB),
|
||||
logbook._generate_filter_from_config(config[logbook.DOMAIN]))
|
||||
entries = list(logbook.humanify(self.hass, events))
|
||||
|
||||
assert 2 == len(entries)
|
||||
assert 4 == len(entries)
|
||||
self.assert_entry(entries[0], name='Home Assistant', message='started',
|
||||
domain=ha.DOMAIN)
|
||||
self.assert_entry(entries[1], pointB, 'blu', domain='sensor',
|
||||
self.assert_entry(entries[1], name='Amazon Alexa', domain='alexa')
|
||||
self.assert_entry(entries[2], name='HomeKit', domain=DOMAIN_HOMEKIT)
|
||||
self.assert_entry(entries[3], pointB, 'blu', domain='sensor',
|
||||
entity_id=entity_id2)
|
||||
|
||||
def test_include_exclude_events(self):
|
||||
|
|
|
@ -55,7 +55,7 @@ def test_recent_items_intent(hass):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_deprecated_api_get_all(hass, aiohttp_client):
|
||||
def test_deprecated_api_get_all(hass, hass_client):
|
||||
"""Test the API."""
|
||||
yield from async_setup_component(hass, 'shopping_list', {})
|
||||
|
||||
|
@ -66,7 +66,7 @@ def test_deprecated_api_get_all(hass, aiohttp_client):
|
|||
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
|
||||
)
|
||||
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
client = yield from hass_client()
|
||||
resp = yield from client.get('/api/shopping_list')
|
||||
|
||||
assert resp.status == 200
|
||||
|
@ -110,7 +110,7 @@ async def test_ws_get_items(hass, hass_ws_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_api_update(hass, aiohttp_client):
|
||||
def test_deprecated_api_update(hass, hass_client):
|
||||
"""Test the API."""
|
||||
yield from async_setup_component(hass, 'shopping_list', {})
|
||||
|
||||
|
@ -124,7 +124,7 @@ def test_api_update(hass, aiohttp_client):
|
|||
beer_id = hass.data['shopping_list'].items[0]['id']
|
||||
wine_id = hass.data['shopping_list'].items[1]['id']
|
||||
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
client = yield from hass_client()
|
||||
resp = yield from client.post(
|
||||
'/api/shopping_list/item/{}'.format(beer_id), json={
|
||||
'name': 'soda'
|
||||
|
@ -164,8 +164,63 @@ def test_api_update(hass, aiohttp_client):
|
|||
}
|
||||
|
||||
|
||||
async def test_ws_update_item(hass, hass_ws_client):
|
||||
"""Test update shopping_list item websocket command."""
|
||||
await async_setup_component(hass, 'shopping_list', {})
|
||||
await intent.async_handle(
|
||||
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
|
||||
)
|
||||
await intent.async_handle(
|
||||
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}}
|
||||
)
|
||||
|
||||
beer_id = hass.data['shopping_list'].items[0]['id']
|
||||
wine_id = hass.data['shopping_list'].items[1]['id']
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({
|
||||
'id': 5,
|
||||
'type': 'shopping_list/items/update',
|
||||
'item_id': beer_id,
|
||||
'name': 'soda'
|
||||
})
|
||||
msg = await client.receive_json()
|
||||
assert msg['success'] is True
|
||||
data = msg['result']
|
||||
assert data == {
|
||||
'id': beer_id,
|
||||
'name': 'soda',
|
||||
'complete': False
|
||||
}
|
||||
await client.send_json({
|
||||
'id': 6,
|
||||
'type': 'shopping_list/items/update',
|
||||
'item_id': wine_id,
|
||||
'complete': True
|
||||
})
|
||||
msg = await client.receive_json()
|
||||
assert msg['success'] is True
|
||||
data = msg['result']
|
||||
assert data == {
|
||||
'id': wine_id,
|
||||
'name': 'wine',
|
||||
'complete': True
|
||||
}
|
||||
|
||||
beer, wine = hass.data['shopping_list'].items
|
||||
assert beer == {
|
||||
'id': beer_id,
|
||||
'name': 'soda',
|
||||
'complete': False
|
||||
}
|
||||
assert wine == {
|
||||
'id': wine_id,
|
||||
'name': 'wine',
|
||||
'complete': True
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_api_update_fails(hass, aiohttp_client):
|
||||
def test_api_update_fails(hass, hass_client):
|
||||
"""Test the API."""
|
||||
yield from async_setup_component(hass, 'shopping_list', {})
|
||||
|
||||
|
@ -173,7 +228,7 @@ def test_api_update_fails(hass, aiohttp_client):
|
|||
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
|
||||
)
|
||||
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
client = yield from hass_client()
|
||||
resp = yield from client.post(
|
||||
'/api/shopping_list/non_existing', json={
|
||||
'name': 'soda'
|
||||
|
@ -190,8 +245,37 @@ def test_api_update_fails(hass, aiohttp_client):
|
|||
assert resp.status == 400
|
||||
|
||||
|
||||
async def test_ws_update_item_fail(hass, hass_ws_client):
|
||||
"""Test failure of update shopping_list item websocket command."""
|
||||
await async_setup_component(hass, 'shopping_list', {})
|
||||
await intent.async_handle(
|
||||
hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}}
|
||||
)
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({
|
||||
'id': 5,
|
||||
'type': 'shopping_list/items/update',
|
||||
'item_id': 'non_existing',
|
||||
'name': 'soda'
|
||||
})
|
||||
msg = await client.receive_json()
|
||||
assert msg['success'] is False
|
||||
data = msg['error']
|
||||
assert data == {
|
||||
'code': 'item_not_found',
|
||||
'message': 'Item not found'
|
||||
}
|
||||
await client.send_json({
|
||||
'id': 6,
|
||||
'type': 'shopping_list/items/update',
|
||||
'name': 123,
|
||||
})
|
||||
msg = await client.receive_json()
|
||||
assert msg['success'] is False
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_api_clear_completed(hass, aiohttp_client):
|
||||
def test_api_clear_completed(hass, hass_client):
|
||||
"""Test the API."""
|
||||
yield from async_setup_component(hass, 'shopping_list', {})
|
||||
|
||||
|
@ -205,7 +289,7 @@ def test_api_clear_completed(hass, aiohttp_client):
|
|||
beer_id = hass.data['shopping_list'].items[0]['id']
|
||||
wine_id = hass.data['shopping_list'].items[1]['id']
|
||||
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
client = yield from hass_client()
|
||||
|
||||
# Mark beer as completed
|
||||
resp = yield from client.post(
|
||||
|
@ -228,11 +312,11 @@ def test_api_clear_completed(hass, aiohttp_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_api_create(hass, aiohttp_client):
|
||||
def test_deprecated_api_create(hass, hass_client):
|
||||
"""Test the API."""
|
||||
yield from async_setup_component(hass, 'shopping_list', {})
|
||||
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
client = yield from hass_client()
|
||||
resp = yield from client.post('/api/shopping_list/item', json={
|
||||
'name': 'soda'
|
||||
})
|
||||
|
@ -249,14 +333,48 @@ def test_api_create(hass, aiohttp_client):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_api_create_fail(hass, aiohttp_client):
|
||||
def test_deprecated_api_create_fail(hass, hass_client):
|
||||
"""Test the API."""
|
||||
yield from async_setup_component(hass, 'shopping_list', {})
|
||||
|
||||
client = yield from aiohttp_client(hass.http.app)
|
||||
client = yield from hass_client()
|
||||
resp = yield from client.post('/api/shopping_list/item', json={
|
||||
'name': 1234
|
||||
})
|
||||
|
||||
assert resp.status == 400
|
||||
assert len(hass.data['shopping_list'].items) == 0
|
||||
|
||||
|
||||
async def test_ws_add_item(hass, hass_ws_client):
|
||||
"""Test adding shopping_list item websocket command."""
|
||||
await async_setup_component(hass, 'shopping_list', {})
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({
|
||||
'id': 5,
|
||||
'type': 'shopping_list/items/add',
|
||||
'name': 'soda',
|
||||
})
|
||||
msg = await client.receive_json()
|
||||
assert msg['success'] is True
|
||||
data = msg['result']
|
||||
assert data['name'] == 'soda'
|
||||
assert data['complete'] is False
|
||||
items = hass.data['shopping_list'].items
|
||||
assert len(items) == 1
|
||||
assert items[0]['name'] == 'soda'
|
||||
assert items[0]['complete'] is False
|
||||
|
||||
|
||||
async def test_ws_add_item_fail(hass, hass_ws_client):
|
||||
"""Test adding shopping_list item failure websocket command."""
|
||||
await async_setup_component(hass, 'shopping_list', {})
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({
|
||||
'id': 5,
|
||||
'type': 'shopping_list/items/add',
|
||||
'name': 123,
|
||||
})
|
||||
msg = await client.receive_json()
|
||||
assert msg['success'] is False
|
||||
assert len(hass.data['shopping_list'].items) == 0
|
||||
|
|
|
@ -56,7 +56,7 @@ SENSOR_OUTPUT = {
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(hass, aiohttp_client):
|
||||
def mock_client(hass, hass_client):
|
||||
"""Start the Home Assistant HTTP component."""
|
||||
with patch('homeassistant.components.spaceapi',
|
||||
return_value=mock_coro(True)):
|
||||
|
@ -70,7 +70,7 @@ def mock_client(hass, aiohttp_client):
|
|||
hass.states.async_set('test.hum1', 88,
|
||||
attributes={'unit_of_measurement': '%'})
|
||||
|
||||
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
|
||||
return hass.loop.run_until_complete(hass_client())
|
||||
|
||||
|
||||
async def test_spaceapi_get(hass, mock_client):
|
||||
|
|
|
@ -14,9 +14,9 @@ BASIC_CONFIG = {
|
|||
}
|
||||
|
||||
|
||||
async def get_error_log(hass, aiohttp_client, expected_count):
|
||||
async def get_error_log(hass, hass_client, expected_count):
|
||||
"""Fetch all entries from system_log via the API."""
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
client = await hass_client()
|
||||
resp = await client.get('/api/error/all')
|
||||
assert resp.status == 200
|
||||
|
||||
|
@ -45,37 +45,37 @@ def get_frame(name):
|
|||
return (name, None, None, None)
|
||||
|
||||
|
||||
async def test_normal_logs(hass, aiohttp_client):
|
||||
async def test_normal_logs(hass, hass_client):
|
||||
"""Test that debug and info are not logged."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.debug('debug')
|
||||
_LOGGER.info('info')
|
||||
|
||||
# Assert done by get_error_log
|
||||
await get_error_log(hass, aiohttp_client, 0)
|
||||
await get_error_log(hass, hass_client, 0)
|
||||
|
||||
|
||||
async def test_exception(hass, aiohttp_client):
|
||||
async def test_exception(hass, hass_client):
|
||||
"""Test that exceptions are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_generate_and_log_exception('exception message', 'log message')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert_log(log, 'exception message', 'log message', 'ERROR')
|
||||
|
||||
|
||||
async def test_warning(hass, aiohttp_client):
|
||||
async def test_warning(hass, hass_client):
|
||||
"""Test that warning are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.warning('warning message')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert_log(log, '', 'warning message', 'WARNING')
|
||||
|
||||
|
||||
async def test_error(hass, aiohttp_client):
|
||||
async def test_error(hass, hass_client):
|
||||
"""Test that errors are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.error('error message')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert_log(log, '', 'error message', 'ERROR')
|
||||
|
||||
|
||||
|
@ -121,26 +121,26 @@ async def test_error_posted_as_event(hass):
|
|||
assert_log(events[0].data, '', 'error message', 'ERROR')
|
||||
|
||||
|
||||
async def test_critical(hass, aiohttp_client):
|
||||
async def test_critical(hass, hass_client):
|
||||
"""Test that critical are logged and retrieved correctly."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.critical('critical message')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert_log(log, '', 'critical message', 'CRITICAL')
|
||||
|
||||
|
||||
async def test_remove_older_logs(hass, aiohttp_client):
|
||||
async def test_remove_older_logs(hass, hass_client):
|
||||
"""Test that older logs are rotated out."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.error('error message 1')
|
||||
_LOGGER.error('error message 2')
|
||||
_LOGGER.error('error message 3')
|
||||
log = await get_error_log(hass, aiohttp_client, 2)
|
||||
log = await get_error_log(hass, hass_client, 2)
|
||||
assert_log(log[0], '', 'error message 3', 'ERROR')
|
||||
assert_log(log[1], '', 'error message 2', 'ERROR')
|
||||
|
||||
|
||||
async def test_clear_logs(hass, aiohttp_client):
|
||||
async def test_clear_logs(hass, hass_client):
|
||||
"""Test that the log can be cleared via a service call."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.error('error message')
|
||||
|
@ -151,7 +151,7 @@ async def test_clear_logs(hass, aiohttp_client):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
# Assert done by get_error_log
|
||||
await get_error_log(hass, aiohttp_client, 0)
|
||||
await get_error_log(hass, hass_client, 0)
|
||||
|
||||
|
||||
async def test_write_log(hass):
|
||||
|
@ -197,13 +197,13 @@ async def test_write_choose_level(hass):
|
|||
assert logger.method_calls[0] == ('debug', ('test_message',))
|
||||
|
||||
|
||||
async def test_unknown_path(hass, aiohttp_client):
|
||||
async def test_unknown_path(hass, hass_client):
|
||||
"""Test error logged from unknown path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
_LOGGER.findCaller = MagicMock(
|
||||
return_value=('unknown_path', 0, None, None))
|
||||
_LOGGER.error('error message')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert log['source'] == 'unknown_path'
|
||||
|
||||
|
||||
|
@ -222,31 +222,31 @@ def log_error_from_test_path(path):
|
|||
_LOGGER.error('error message')
|
||||
|
||||
|
||||
async def test_homeassistant_path(hass, aiohttp_client):
|
||||
async def test_homeassistant_path(hass, hass_client):
|
||||
"""Test error logged from homeassistant path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH',
|
||||
new=['venv_path/homeassistant']):
|
||||
log_error_from_test_path(
|
||||
'venv_path/homeassistant/component/component.py')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert log['source'] == 'component/component.py'
|
||||
|
||||
|
||||
async def test_config_path(hass, aiohttp_client):
|
||||
async def test_config_path(hass, hass_client):
|
||||
"""Test error logged from config path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
with patch.object(hass.config, 'config_dir', new='config'):
|
||||
log_error_from_test_path('config/custom_component/test.py')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert log['source'] == 'custom_component/test.py'
|
||||
|
||||
|
||||
async def test_netdisco_path(hass, aiohttp_client):
|
||||
async def test_netdisco_path(hass, hass_client):
|
||||
"""Test error logged from netdisco path."""
|
||||
await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG)
|
||||
with patch.dict('sys.modules',
|
||||
netdisco=MagicMock(__path__=['venv_path/netdisco'])):
|
||||
log_error_from_test_path('venv_path/netdisco/disco_component.py')
|
||||
log = (await get_error_log(hass, aiohttp_client, 1))[0]
|
||||
log = (await get_error_log(hass, hass_client, 1))[0]
|
||||
assert log['source'] == 'disco_component.py'
|
||||
|
|
|
@ -7,10 +7,10 @@ from homeassistant.setup import async_setup_component
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(hass, aiohttp_client):
|
||||
def mock_client(hass, hass_client):
|
||||
"""Create http client for webhooks."""
|
||||
hass.loop.run_until_complete(async_setup_component(hass, 'webhook', {}))
|
||||
return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
|
||||
return hass.loop.run_until_complete(hass_client())
|
||||
|
||||
|
||||
async def test_unregistering_webhook(hass, mock_client):
|
||||
|
|
|
@ -431,3 +431,24 @@ async def test_update_entity(hass):
|
|||
|
||||
assert len(entity.async_update_ha_state.mock_calls) == 2
|
||||
assert entity.async_update_ha_state.mock_calls[-1][1][0] is True
|
||||
|
||||
|
||||
async def test_set_service_race(hass):
|
||||
"""Test race condition on setting service."""
|
||||
exception = False
|
||||
|
||||
def async_loop_exception_handler(_, _2) -> None:
|
||||
"""Handle all exception inside the core loop."""
|
||||
nonlocal exception
|
||||
exception = True
|
||||
|
||||
hass.loop.set_exception_handler(async_loop_exception_handler)
|
||||
|
||||
await async_setup_component(hass, 'group', {})
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass, group_name='yo')
|
||||
|
||||
for i in range(2):
|
||||
hass.async_create_task(component.async_add_entities([MockEntity()]))
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert not exception
|
||||
|
|
Loading…
Reference in New Issue