Merge pull request from home-assistant/rc

0.83.1
pull/18947/head 0.83.1
Paulus Schoutsen 2018-11-29 23:18:13 +01:00 committed by GitHub
commit 3701c0f219
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 490 additions and 178 deletions

View File

@ -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)

View File

@ -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."""

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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": [{

View File

@ -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(

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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, )

View File

@ -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 "

View File

@ -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)

View File

@ -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'

View File

@ -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'))

View File

@ -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)

View File

@ -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

View File

@ -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,
},
]

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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"}))), \

View File

@ -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)

View File

@ -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()

View File

@ -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': {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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'

View File

@ -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):

View File

@ -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