RFC: Call services directly (#18720)
* Call services directly * Simplify * Type * Lint * Update name * Fix tests * Catch exceptions in HTTP view * Lint * Handle ServiceNotFound in API endpoints that call services * Type * Don't crash recorder on non-JSON serializable objects
This commit is contained in:
parent
53cbb28926
commit
df21dd21f2
30 changed files with 312 additions and 186 deletions
|
@ -11,6 +11,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
|
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||||
|
@ -314,8 +315,11 @@ class NotifySetupFlow(SetupFlow):
|
||||||
_generate_otp, self._secret, self._count)
|
_generate_otp, self._secret, self._count)
|
||||||
|
|
||||||
assert self._notify_service
|
assert self._notify_service
|
||||||
await self._auth_module.async_notify(
|
try:
|
||||||
code, self._notify_service, self._target)
|
await self._auth_module.async_notify(
|
||||||
|
code, self._notify_service, self._target)
|
||||||
|
except ServiceNotFound:
|
||||||
|
return self.async_abort(reason='notify_service_not_exist')
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='setup',
|
step_id='setup',
|
||||||
|
|
|
@ -226,7 +226,11 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
|
|
||||||
if user_input is None and hasattr(auth_module,
|
if user_input is None and hasattr(auth_module,
|
||||||
'async_initialize_login_mfa_step'):
|
'async_initialize_login_mfa_step'):
|
||||||
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
try:
|
||||||
|
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
||||||
|
except HomeAssistantError:
|
||||||
|
_LOGGER.exception('Error initializing MFA step')
|
||||||
|
return self.async_abort(reason='unknown_error')
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
expires = self.created_at + MFA_SESSION_EXPIRATION
|
expires = self.created_at + MFA_SESSION_EXPIRATION
|
||||||
|
|
|
@ -9,7 +9,9 @@ import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp.web_exceptions import HTTPBadRequest
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.bootstrap import DATA_LOGGING
|
from homeassistant.bootstrap import DATA_LOGGING
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
@ -21,7 +23,8 @@ from homeassistant.const import (
|
||||||
URL_API_TEMPLATE, __version__)
|
URL_API_TEMPLATE, __version__)
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.auth.permissions.const import POLICY_READ
|
from homeassistant.auth.permissions.const import POLICY_READ
|
||||||
from homeassistant.exceptions import TemplateError, Unauthorized
|
from homeassistant.exceptions import (
|
||||||
|
TemplateError, Unauthorized, ServiceNotFound)
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
from homeassistant.helpers.service import async_get_all_descriptions
|
from homeassistant.helpers.service import async_get_all_descriptions
|
||||||
from homeassistant.helpers.state import AsyncTrackStates
|
from homeassistant.helpers.state import AsyncTrackStates
|
||||||
|
@ -339,8 +342,11 @@ class APIDomainServicesView(HomeAssistantView):
|
||||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
with AsyncTrackStates(hass) as changed_states:
|
with AsyncTrackStates(hass) as changed_states:
|
||||||
await hass.services.async_call(
|
try:
|
||||||
domain, service, data, True, self.context(request))
|
await hass.services.async_call(
|
||||||
|
domain, service, data, True, self.context(request))
|
||||||
|
except (vol.Invalid, ServiceNotFound):
|
||||||
|
raise HTTPBadRequest()
|
||||||
|
|
||||||
return self.json(changed_states)
|
return self.json(changed_states)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,9 @@ import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPInternalServerError
|
from aiohttp.web_exceptions import (
|
||||||
|
HTTPUnauthorized, HTTPInternalServerError, HTTPBadRequest)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.http.ban import process_success_login
|
from homeassistant.components.http.ban import process_success_login
|
||||||
from homeassistant.core import Context, is_callback
|
from homeassistant.core import Context, is_callback
|
||||||
|
@ -114,6 +116,10 @@ def request_handler_factory(view, handler):
|
||||||
|
|
||||||
if asyncio.iscoroutine(result):
|
if asyncio.iscoroutine(result):
|
||||||
result = await result
|
result = await result
|
||||||
|
except vol.Invalid:
|
||||||
|
raise HTTPBadRequest()
|
||||||
|
except exceptions.ServiceNotFound:
|
||||||
|
raise HTTPInternalServerError()
|
||||||
except exceptions.Unauthorized:
|
except exceptions.Unauthorized:
|
||||||
raise HTTPUnauthorized()
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.core import callback
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
valid_publish_topic, valid_subscribe_topic)
|
valid_publish_topic, valid_subscribe_topic)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_SERVICE_EXECUTED,
|
ATTR_SERVICE_DATA, EVENT_CALL_SERVICE,
|
||||||
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL)
|
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL)
|
||||||
from homeassistant.core import EventOrigin, State
|
from homeassistant.core import EventOrigin, State
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -69,16 +69,6 @@ def async_setup(hass, config):
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Filter out all the "event service executed" events because they
|
|
||||||
# are only used internally by core as callbacks for blocking
|
|
||||||
# during the interval while a service is being executed.
|
|
||||||
# They will serve no purpose to the external system,
|
|
||||||
# and thus are unnecessary traffic.
|
|
||||||
# And at any rate it would cause an infinite loop to publish them
|
|
||||||
# because publishing to an MQTT topic itself triggers one.
|
|
||||||
if event.event_type == EVENT_SERVICE_EXECUTED:
|
|
||||||
return
|
|
||||||
|
|
||||||
event_info = {'event_type': event.event_type, 'event_data': event.data}
|
event_info = {'event_type': event.event_type, 'event_data': event.data}
|
||||||
msg = json.dumps(event_info, cls=JSONEncoder)
|
msg = json.dumps(event_info, cls=JSONEncoder)
|
||||||
mqtt.async_publish(pub_topic, msg)
|
mqtt.async_publish(pub_topic, msg)
|
||||||
|
|
|
@ -300,14 +300,24 @@ class Recorder(threading.Thread):
|
||||||
time.sleep(CONNECT_RETRY_WAIT)
|
time.sleep(CONNECT_RETRY_WAIT)
|
||||||
try:
|
try:
|
||||||
with session_scope(session=self.get_session()) as session:
|
with session_scope(session=self.get_session()) as session:
|
||||||
dbevent = Events.from_event(event)
|
try:
|
||||||
session.add(dbevent)
|
dbevent = Events.from_event(event)
|
||||||
session.flush()
|
session.add(dbevent)
|
||||||
|
session.flush()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Event is not JSON serializable: %s", event)
|
||||||
|
|
||||||
if event.event_type == EVENT_STATE_CHANGED:
|
if event.event_type == EVENT_STATE_CHANGED:
|
||||||
dbstate = States.from_event(event)
|
try:
|
||||||
dbstate.event_id = dbevent.event_id
|
dbstate = States.from_event(event)
|
||||||
session.add(dbstate)
|
dbstate.event_id = dbevent.event_id
|
||||||
|
session.add(dbstate)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"State is not JSON serializable: %s",
|
||||||
|
event.data.get('new_state'))
|
||||||
|
|
||||||
updated = True
|
updated = True
|
||||||
|
|
||||||
except exc.OperationalError as err:
|
except exc.OperationalError as err:
|
||||||
|
|
|
@ -3,7 +3,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED
|
from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED
|
||||||
from homeassistant.core import callback, DOMAIN as HASS_DOMAIN
|
from homeassistant.core import callback, DOMAIN as HASS_DOMAIN
|
||||||
from homeassistant.exceptions import Unauthorized
|
from homeassistant.exceptions import Unauthorized, ServiceNotFound
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.service import async_get_all_descriptions
|
from homeassistant.helpers.service import async_get_all_descriptions
|
||||||
|
|
||||||
|
@ -141,10 +141,15 @@ async def handle_call_service(hass, connection, msg):
|
||||||
if (msg['domain'] == HASS_DOMAIN and
|
if (msg['domain'] == HASS_DOMAIN and
|
||||||
msg['service'] in ['restart', 'stop']):
|
msg['service'] in ['restart', 'stop']):
|
||||||
blocking = False
|
blocking = False
|
||||||
await hass.services.async_call(
|
|
||||||
msg['domain'], msg['service'], msg.get('service_data'), blocking,
|
try:
|
||||||
connection.context(msg))
|
await hass.services.async_call(
|
||||||
connection.send_message(messages.result_message(msg['id']))
|
msg['domain'], msg['service'], msg.get('service_data'), blocking,
|
||||||
|
connection.context(msg))
|
||||||
|
connection.send_message(messages.result_message(msg['id']))
|
||||||
|
except ServiceNotFound:
|
||||||
|
connection.send_message(messages.error_message(
|
||||||
|
msg['id'], const.ERR_NOT_FOUND, 'Service not found.'))
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -163,7 +163,6 @@ EVENT_HOMEASSISTANT_CLOSE = 'homeassistant_close'
|
||||||
EVENT_STATE_CHANGED = 'state_changed'
|
EVENT_STATE_CHANGED = 'state_changed'
|
||||||
EVENT_TIME_CHANGED = 'time_changed'
|
EVENT_TIME_CHANGED = 'time_changed'
|
||||||
EVENT_CALL_SERVICE = 'call_service'
|
EVENT_CALL_SERVICE = 'call_service'
|
||||||
EVENT_SERVICE_EXECUTED = 'service_executed'
|
|
||||||
EVENT_PLATFORM_DISCOVERED = 'platform_discovered'
|
EVENT_PLATFORM_DISCOVERED = 'platform_discovered'
|
||||||
EVENT_COMPONENT_LOADED = 'component_loaded'
|
EVENT_COMPONENT_LOADED = 'component_loaded'
|
||||||
EVENT_SERVICE_REGISTERED = 'service_registered'
|
EVENT_SERVICE_REGISTERED = 'service_registered'
|
||||||
|
@ -233,9 +232,6 @@ ATTR_ID = 'id'
|
||||||
# Name
|
# Name
|
||||||
ATTR_NAME = 'name'
|
ATTR_NAME = 'name'
|
||||||
|
|
||||||
# Data for a SERVICE_EXECUTED event
|
|
||||||
ATTR_SERVICE_CALL_ID = 'service_call_id'
|
|
||||||
|
|
||||||
# Contains one string or a list of strings, each being an entity id
|
# Contains one string or a list of strings, each being an entity id
|
||||||
ATTR_ENTITY_ID = 'entity_id'
|
ATTR_ENTITY_ID = 'entity_id'
|
||||||
|
|
||||||
|
|
|
@ -25,18 +25,18 @@ from typing import ( # noqa: F401 pylint: disable=unused-import
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
import attr
|
import attr
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE,
|
ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, ATTR_SERVICE,
|
||||||
ATTR_SERVICE_CALL_ID, ATTR_SERVICE_DATA, ATTR_SECONDS, EVENT_CALL_SERVICE,
|
ATTR_SERVICE_DATA, ATTR_SECONDS, EVENT_CALL_SERVICE,
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED,
|
EVENT_HOMEASSISTANT_CLOSE, EVENT_SERVICE_REMOVED,
|
||||||
EVENT_SERVICE_EXECUTED, EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED,
|
EVENT_SERVICE_REGISTERED, EVENT_STATE_CHANGED,
|
||||||
EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__)
|
EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__)
|
||||||
from homeassistant import loader
|
from homeassistant import loader
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError, InvalidEntityFormatError, InvalidStateError)
|
HomeAssistantError, InvalidEntityFormatError, InvalidStateError,
|
||||||
|
Unauthorized, ServiceNotFound)
|
||||||
from homeassistant.util.async_ import (
|
from homeassistant.util.async_ import (
|
||||||
run_coroutine_threadsafe, run_callback_threadsafe,
|
run_coroutine_threadsafe, run_callback_threadsafe,
|
||||||
fire_coroutine_threadsafe)
|
fire_coroutine_threadsafe)
|
||||||
|
@ -954,7 +954,6 @@ class ServiceRegistry:
|
||||||
"""Initialize a service registry."""
|
"""Initialize a service registry."""
|
||||||
self._services = {} # type: Dict[str, Dict[str, Service]]
|
self._services = {} # type: Dict[str, Dict[str, Service]]
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._async_unsub_call_event = None # type: Optional[CALLBACK_TYPE]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self) -> Dict[str, Dict[str, Service]]:
|
def services(self) -> Dict[str, Dict[str, Service]]:
|
||||||
|
@ -1010,10 +1009,6 @@ class ServiceRegistry:
|
||||||
else:
|
else:
|
||||||
self._services[domain] = {service: service_obj}
|
self._services[domain] = {service: service_obj}
|
||||||
|
|
||||||
if self._async_unsub_call_event is None:
|
|
||||||
self._async_unsub_call_event = self._hass.bus.async_listen(
|
|
||||||
EVENT_CALL_SERVICE, self._event_to_service_call)
|
|
||||||
|
|
||||||
self._hass.bus.async_fire(
|
self._hass.bus.async_fire(
|
||||||
EVENT_SERVICE_REGISTERED,
|
EVENT_SERVICE_REGISTERED,
|
||||||
{ATTR_DOMAIN: domain, ATTR_SERVICE: service}
|
{ATTR_DOMAIN: domain, ATTR_SERVICE: service}
|
||||||
|
@ -1092,100 +1087,61 @@ class ServiceRegistry:
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
|
domain = domain.lower()
|
||||||
|
service = service.lower()
|
||||||
context = context or Context()
|
context = context or Context()
|
||||||
call_id = uuid.uuid4().hex
|
service_data = service_data or {}
|
||||||
event_data = {
|
|
||||||
|
try:
|
||||||
|
handler = self._services[domain][service]
|
||||||
|
except KeyError:
|
||||||
|
raise ServiceNotFound(domain, service) from None
|
||||||
|
|
||||||
|
if handler.schema:
|
||||||
|
service_data = handler.schema(service_data)
|
||||||
|
|
||||||
|
service_call = ServiceCall(domain, service, service_data, context)
|
||||||
|
|
||||||
|
self._hass.bus.async_fire(EVENT_CALL_SERVICE, {
|
||||||
ATTR_DOMAIN: domain.lower(),
|
ATTR_DOMAIN: domain.lower(),
|
||||||
ATTR_SERVICE: service.lower(),
|
ATTR_SERVICE: service.lower(),
|
||||||
ATTR_SERVICE_DATA: service_data,
|
ATTR_SERVICE_DATA: service_data,
|
||||||
ATTR_SERVICE_CALL_ID: call_id,
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if not blocking:
|
if not blocking:
|
||||||
self._hass.bus.async_fire(
|
self._hass.async_create_task(
|
||||||
EVENT_CALL_SERVICE, event_data, EventOrigin.local, context)
|
self._safe_execute(handler, service_call))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fut = asyncio.Future() # type: asyncio.Future
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def service_executed(event: Event) -> None:
|
|
||||||
"""Handle an executed service."""
|
|
||||||
if event.data[ATTR_SERVICE_CALL_ID] == call_id:
|
|
||||||
fut.set_result(True)
|
|
||||||
unsub()
|
|
||||||
|
|
||||||
unsub = self._hass.bus.async_listen(
|
|
||||||
EVENT_SERVICE_EXECUTED, service_executed)
|
|
||||||
|
|
||||||
self._hass.bus.async_fire(EVENT_CALL_SERVICE, event_data,
|
|
||||||
EventOrigin.local, context)
|
|
||||||
|
|
||||||
done, _ = await asyncio.wait([fut], timeout=SERVICE_CALL_LIMIT)
|
|
||||||
success = bool(done)
|
|
||||||
if not success:
|
|
||||||
unsub()
|
|
||||||
return success
|
|
||||||
|
|
||||||
async def _event_to_service_call(self, event: Event) -> None:
|
|
||||||
"""Handle the SERVICE_CALLED events from the EventBus."""
|
|
||||||
service_data = event.data.get(ATTR_SERVICE_DATA) or {}
|
|
||||||
domain = event.data.get(ATTR_DOMAIN).lower() # type: ignore
|
|
||||||
service = event.data.get(ATTR_SERVICE).lower() # type: ignore
|
|
||||||
call_id = event.data.get(ATTR_SERVICE_CALL_ID)
|
|
||||||
|
|
||||||
if not self.has_service(domain, service):
|
|
||||||
if event.origin == EventOrigin.local:
|
|
||||||
_LOGGER.warning("Unable to find service %s/%s",
|
|
||||||
domain, service)
|
|
||||||
return
|
|
||||||
|
|
||||||
service_handler = self._services[domain][service]
|
|
||||||
|
|
||||||
def fire_service_executed() -> None:
|
|
||||||
"""Fire service executed event."""
|
|
||||||
if not call_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
data = {ATTR_SERVICE_CALL_ID: call_id}
|
|
||||||
|
|
||||||
if (service_handler.is_coroutinefunction or
|
|
||||||
service_handler.is_callback):
|
|
||||||
self._hass.bus.async_fire(EVENT_SERVICE_EXECUTED, data,
|
|
||||||
EventOrigin.local, event.context)
|
|
||||||
else:
|
|
||||||
self._hass.bus.fire(EVENT_SERVICE_EXECUTED, data,
|
|
||||||
EventOrigin.local, event.context)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if service_handler.schema:
|
with timeout(SERVICE_CALL_LIMIT):
|
||||||
service_data = service_handler.schema(service_data)
|
await asyncio.shield(
|
||||||
except vol.Invalid as ex:
|
self._execute_service(handler, service_call))
|
||||||
_LOGGER.error("Invalid service data for %s.%s: %s",
|
return True
|
||||||
domain, service, humanize_error(service_data, ex))
|
except asyncio.TimeoutError:
|
||||||
fire_service_executed()
|
return False
|
||||||
return
|
|
||||||
|
|
||||||
service_call = ServiceCall(
|
|
||||||
domain, service, service_data, event.context)
|
|
||||||
|
|
||||||
|
async def _safe_execute(self, handler: Service,
|
||||||
|
service_call: ServiceCall) -> None:
|
||||||
|
"""Execute a service and catch exceptions."""
|
||||||
try:
|
try:
|
||||||
if service_handler.is_callback:
|
await self._execute_service(handler, service_call)
|
||||||
service_handler.func(service_call)
|
except Unauthorized:
|
||||||
fire_service_executed()
|
_LOGGER.warning('Unauthorized service called %s/%s',
|
||||||
elif service_handler.is_coroutinefunction:
|
service_call.domain, service_call.service)
|
||||||
await service_handler.func(service_call)
|
|
||||||
fire_service_executed()
|
|
||||||
else:
|
|
||||||
def execute_service() -> None:
|
|
||||||
"""Execute a service and fires a SERVICE_EXECUTED event."""
|
|
||||||
service_handler.func(service_call)
|
|
||||||
fire_service_executed()
|
|
||||||
|
|
||||||
await self._hass.async_add_executor_job(execute_service)
|
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception('Error executing service %s', service_call)
|
_LOGGER.exception('Error executing service %s', service_call)
|
||||||
|
|
||||||
|
async def _execute_service(self, handler: Service,
|
||||||
|
service_call: ServiceCall) -> None:
|
||||||
|
"""Execute a service."""
|
||||||
|
if handler.is_callback:
|
||||||
|
handler.func(service_call)
|
||||||
|
elif handler.is_coroutinefunction:
|
||||||
|
await handler.func(service_call)
|
||||||
|
else:
|
||||||
|
await self._hass.async_add_executor_job(handler.func, service_call)
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Configuration settings for Home Assistant."""
|
"""Configuration settings for Home Assistant."""
|
||||||
|
|
|
@ -58,3 +58,14 @@ class Unauthorized(HomeAssistantError):
|
||||||
|
|
||||||
class UnknownUser(Unauthorized):
|
class UnknownUser(Unauthorized):
|
||||||
"""When call is made with user ID that doesn't exist."""
|
"""When call is made with user ID that doesn't exist."""
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceNotFound(HomeAssistantError):
|
||||||
|
"""Raised when a service is not found."""
|
||||||
|
|
||||||
|
def __init__(self, domain: str, service: str) -> None:
|
||||||
|
"""Initialize error."""
|
||||||
|
super().__init__(
|
||||||
|
self, "Service {}.{} not found".format(domain, service))
|
||||||
|
self.domain = domain
|
||||||
|
self.service = service
|
||||||
|
|
|
@ -61,6 +61,7 @@ async def test_validating_mfa_counter(hass):
|
||||||
'counter': 0,
|
'counter': 0,
|
||||||
'notify_service': 'dummy',
|
'notify_service': 'dummy',
|
||||||
})
|
})
|
||||||
|
async_mock_service(hass, 'notify', 'dummy')
|
||||||
|
|
||||||
assert notify_auth_module._user_settings
|
assert notify_auth_module._user_settings
|
||||||
notify_setting = list(notify_auth_module._user_settings.values())[0]
|
notify_setting = list(notify_auth_module._user_settings.values())[0]
|
||||||
|
@ -389,9 +390,8 @@ async def test_not_raise_exception_when_service_not_exist(hass):
|
||||||
'username': 'test-user',
|
'username': 'test-user',
|
||||||
'password': 'test-pass',
|
'password': 'test-pass',
|
||||||
})
|
})
|
||||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
assert result['step_id'] == 'mfa'
|
assert result['reason'] == 'unknown_error'
|
||||||
assert result['data_schema'].schema.get('code') == str
|
|
||||||
|
|
||||||
# wait service call finished
|
# wait service call finished
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""The tests for the demo climate component."""
|
"""The tests for the demo climate component."""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.util.unit_system import (
|
from homeassistant.util.unit_system import (
|
||||||
METRIC_SYSTEM
|
METRIC_SYSTEM
|
||||||
)
|
)
|
||||||
|
@ -57,7 +60,8 @@ class TestDemoClimate(unittest.TestCase):
|
||||||
"""Test setting the target temperature without required attribute."""
|
"""Test setting the target temperature without required attribute."""
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert 21 == state.attributes.get('temperature')
|
assert 21 == state.attributes.get('temperature')
|
||||||
common.set_temperature(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_temperature(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
assert 21 == state.attributes.get('temperature')
|
assert 21 == state.attributes.get('temperature')
|
||||||
|
|
||||||
|
@ -99,9 +103,11 @@ class TestDemoClimate(unittest.TestCase):
|
||||||
assert state.attributes.get('temperature') is None
|
assert state.attributes.get('temperature') is None
|
||||||
assert 21.0 == state.attributes.get('target_temp_low')
|
assert 21.0 == state.attributes.get('target_temp_low')
|
||||||
assert 24.0 == state.attributes.get('target_temp_high')
|
assert 24.0 == state.attributes.get('target_temp_high')
|
||||||
common.set_temperature(self.hass, temperature=None,
|
with pytest.raises(vol.Invalid):
|
||||||
entity_id=ENTITY_ECOBEE, target_temp_low=None,
|
common.set_temperature(self.hass, temperature=None,
|
||||||
target_temp_high=None)
|
entity_id=ENTITY_ECOBEE,
|
||||||
|
target_temp_low=None,
|
||||||
|
target_temp_high=None)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_ECOBEE)
|
state = self.hass.states.get(ENTITY_ECOBEE)
|
||||||
assert state.attributes.get('temperature') is None
|
assert state.attributes.get('temperature') is None
|
||||||
|
@ -112,7 +118,8 @@ class TestDemoClimate(unittest.TestCase):
|
||||||
"""Test setting the target humidity without required attribute."""
|
"""Test setting the target humidity without required attribute."""
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert 67 == state.attributes.get('humidity')
|
assert 67 == state.attributes.get('humidity')
|
||||||
common.set_humidity(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_humidity(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert 67 == state.attributes.get('humidity')
|
assert 67 == state.attributes.get('humidity')
|
||||||
|
@ -130,7 +137,8 @@ class TestDemoClimate(unittest.TestCase):
|
||||||
"""Test setting fan mode without required attribute."""
|
"""Test setting fan mode without required attribute."""
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "On High" == state.attributes.get('fan_mode')
|
assert "On High" == state.attributes.get('fan_mode')
|
||||||
common.set_fan_mode(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_fan_mode(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "On High" == state.attributes.get('fan_mode')
|
assert "On High" == state.attributes.get('fan_mode')
|
||||||
|
@ -148,7 +156,8 @@ class TestDemoClimate(unittest.TestCase):
|
||||||
"""Test setting swing mode without required attribute."""
|
"""Test setting swing mode without required attribute."""
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "Off" == state.attributes.get('swing_mode')
|
assert "Off" == state.attributes.get('swing_mode')
|
||||||
common.set_swing_mode(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_swing_mode(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "Off" == state.attributes.get('swing_mode')
|
assert "Off" == state.attributes.get('swing_mode')
|
||||||
|
@ -170,7 +179,8 @@ class TestDemoClimate(unittest.TestCase):
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "cool" == state.attributes.get('operation_mode')
|
assert "cool" == state.attributes.get('operation_mode')
|
||||||
assert "cool" == state.state
|
assert "cool" == state.state
|
||||||
common.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "cool" == state.attributes.get('operation_mode')
|
assert "cool" == state.attributes.get('operation_mode')
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""The tests for the climate component."""
|
"""The tests for the climate component."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA
|
from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
|
@ -14,12 +17,11 @@ def test_set_temp_schema_no_req(hass, caplog):
|
||||||
calls = async_mock_service(hass, domain, service, schema)
|
calls = async_mock_service(hass, domain, service, schema)
|
||||||
|
|
||||||
data = {'operation_mode': 'test', 'entity_id': ['climate.test_id']}
|
data = {'operation_mode': 'test', 'entity_id': ['climate.test_id']}
|
||||||
yield from hass.services.async_call(domain, service, data)
|
with pytest.raises(vol.Invalid):
|
||||||
|
yield from hass.services.async_call(domain, service, data)
|
||||||
yield from hass.async_block_till_done()
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
assert 'ERROR' in caplog.text
|
|
||||||
assert 'Invalid service data' in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
import unittest
|
import unittest
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.util.unit_system import (
|
from homeassistant.util.unit_system import (
|
||||||
METRIC_SYSTEM
|
METRIC_SYSTEM
|
||||||
)
|
)
|
||||||
|
@ -91,7 +94,8 @@ class TestMQTTClimate(unittest.TestCase):
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "off" == state.attributes.get('operation_mode')
|
assert "off" == state.attributes.get('operation_mode')
|
||||||
assert "off" == state.state
|
assert "off" == state.state
|
||||||
common.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "off" == state.attributes.get('operation_mode')
|
assert "off" == state.attributes.get('operation_mode')
|
||||||
|
@ -177,7 +181,8 @@ class TestMQTTClimate(unittest.TestCase):
|
||||||
|
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "low" == state.attributes.get('fan_mode')
|
assert "low" == state.attributes.get('fan_mode')
|
||||||
common.set_fan_mode(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_fan_mode(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "low" == state.attributes.get('fan_mode')
|
assert "low" == state.attributes.get('fan_mode')
|
||||||
|
@ -225,7 +230,8 @@ class TestMQTTClimate(unittest.TestCase):
|
||||||
|
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "off" == state.attributes.get('swing_mode')
|
assert "off" == state.attributes.get('swing_mode')
|
||||||
common.set_swing_mode(self.hass, None, ENTITY_CLIMATE)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_swing_mode(self.hass, None, ENTITY_CLIMATE)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_CLIMATE)
|
state = self.hass.states.get(ENTITY_CLIMATE)
|
||||||
assert "off" == state.attributes.get('swing_mode')
|
assert "off" == state.attributes.get('swing_mode')
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""Test deCONZ component setup process."""
|
"""Test deCONZ component setup process."""
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components import deconz
|
from homeassistant.components import deconz
|
||||||
|
|
||||||
|
@ -163,11 +166,13 @@ async def test_service_configure(hass):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# field does not start with /
|
# field does not start with /
|
||||||
with patch('pydeconz.DeconzSession.async_put_state',
|
with pytest.raises(vol.Invalid):
|
||||||
return_value=mock_coro(True)):
|
with patch('pydeconz.DeconzSession.async_put_state',
|
||||||
await hass.services.async_call('deconz', 'configure', service_data={
|
return_value=mock_coro(True)):
|
||||||
'entity': 'light.test', 'field': 'state', 'data': data})
|
await hass.services.async_call(
|
||||||
await hass.async_block_till_done()
|
'deconz', 'configure', service_data={
|
||||||
|
'entity': 'light.test', 'field': 'state', 'data': data})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
async def test_service_refresh_devices(hass):
|
async def test_service_refresh_devices(hass):
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
"""Tests for Home Assistant View."""
|
"""Tests for Home Assistant View."""
|
||||||
from aiohttp.web_exceptions import HTTPInternalServerError
|
from unittest.mock import Mock
|
||||||
import pytest
|
|
||||||
|
|
||||||
from homeassistant.components.http.view import HomeAssistantView
|
from aiohttp.web_exceptions import (
|
||||||
|
HTTPInternalServerError, HTTPBadRequest, HTTPUnauthorized)
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.http.view import (
|
||||||
|
HomeAssistantView, request_handler_factory)
|
||||||
|
from homeassistant.exceptions import ServiceNotFound, Unauthorized
|
||||||
|
|
||||||
|
from tests.common import mock_coro_func
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_request():
|
||||||
|
"""Mock a request."""
|
||||||
|
return Mock(
|
||||||
|
app={'hass': Mock(is_running=True)},
|
||||||
|
match_info={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_invalid_json(caplog):
|
async def test_invalid_json(caplog):
|
||||||
|
@ -13,3 +30,30 @@ async def test_invalid_json(caplog):
|
||||||
view.json(float("NaN"))
|
view.json(float("NaN"))
|
||||||
|
|
||||||
assert str(float("NaN")) in caplog.text
|
assert str(float("NaN")) in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_handling_unauthorized(mock_request):
|
||||||
|
"""Test handling unauth exceptions."""
|
||||||
|
with pytest.raises(HTTPUnauthorized):
|
||||||
|
await request_handler_factory(
|
||||||
|
Mock(requires_auth=False),
|
||||||
|
mock_coro_func(exception=Unauthorized)
|
||||||
|
)(mock_request)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_handling_invalid_data(mock_request):
|
||||||
|
"""Test handling unauth exceptions."""
|
||||||
|
with pytest.raises(HTTPBadRequest):
|
||||||
|
await request_handler_factory(
|
||||||
|
Mock(requires_auth=False),
|
||||||
|
mock_coro_func(exception=vol.Invalid('yo'))
|
||||||
|
)(mock_request)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_handling_service_not_found(mock_request):
|
||||||
|
"""Test handling unauth exceptions."""
|
||||||
|
with pytest.raises(HTTPInternalServerError):
|
||||||
|
await request_handler_factory(
|
||||||
|
Mock(requires_auth=False),
|
||||||
|
mock_coro_func(exception=ServiceNotFound('test', 'test'))
|
||||||
|
)(mock_request)
|
||||||
|
|
|
@ -3,6 +3,9 @@ import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.setup import setup_component
|
from homeassistant.setup import setup_component
|
||||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||||
import homeassistant.components.media_player as mp
|
import homeassistant.components.media_player as mp
|
||||||
|
@ -43,7 +46,8 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||||
state = self.hass.states.get(entity_id)
|
state = self.hass.states.get(entity_id)
|
||||||
assert 'dvd' == state.attributes.get('source')
|
assert 'dvd' == state.attributes.get('source')
|
||||||
|
|
||||||
common.select_source(self.hass, None, entity_id)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.select_source(self.hass, None, entity_id)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(entity_id)
|
state = self.hass.states.get(entity_id)
|
||||||
assert 'dvd' == state.attributes.get('source')
|
assert 'dvd' == state.attributes.get('source')
|
||||||
|
@ -72,7 +76,8 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||||
state = self.hass.states.get(entity_id)
|
state = self.hass.states.get(entity_id)
|
||||||
assert 1.0 == state.attributes.get('volume_level')
|
assert 1.0 == state.attributes.get('volume_level')
|
||||||
|
|
||||||
common.set_volume_level(self.hass, None, entity_id)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_volume_level(self.hass, None, entity_id)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(entity_id)
|
state = self.hass.states.get(entity_id)
|
||||||
assert 1.0 == state.attributes.get('volume_level')
|
assert 1.0 == state.attributes.get('volume_level')
|
||||||
|
@ -201,7 +206,8 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||||
state.attributes.get('supported_features'))
|
state.attributes.get('supported_features'))
|
||||||
assert state.attributes.get('media_content_id') is not None
|
assert state.attributes.get('media_content_id') is not None
|
||||||
|
|
||||||
common.play_media(self.hass, None, 'some_id', ent_id)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.play_media(self.hass, None, 'some_id', ent_id)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ent_id)
|
state = self.hass.states.get(ent_id)
|
||||||
assert 0 < (mp.SUPPORT_PLAY_MEDIA &
|
assert 0 < (mp.SUPPORT_PLAY_MEDIA &
|
||||||
|
@ -216,7 +222,8 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||||
assert 'some_id' == state.attributes.get('media_content_id')
|
assert 'some_id' == state.attributes.get('media_content_id')
|
||||||
|
|
||||||
assert not mock_seek.called
|
assert not mock_seek.called
|
||||||
common.media_seek(self.hass, None, ent_id)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.media_seek(self.hass, None, ent_id)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
assert not mock_seek.called
|
assert not mock_seek.called
|
||||||
common.media_seek(self.hass, 100, ent_id)
|
common.media_seek(self.hass, 100, ent_id)
|
||||||
|
|
|
@ -223,7 +223,7 @@ class TestMonopriceMediaPlayer(unittest.TestCase):
|
||||||
# Restoring wrong media player to its previous state
|
# Restoring wrong media player to its previous state
|
||||||
# Nothing should be done
|
# Nothing should be done
|
||||||
self.hass.services.call(DOMAIN, SERVICE_RESTORE,
|
self.hass.services.call(DOMAIN, SERVICE_RESTORE,
|
||||||
{'entity_id': 'not_existing'},
|
{'entity_id': 'media.not_existing'},
|
||||||
blocking=True)
|
blocking=True)
|
||||||
# self.hass.block_till_done()
|
# self.hass.block_till_done()
|
||||||
|
|
||||||
|
|
|
@ -113,11 +113,12 @@ class TestMQTTComponent(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
payload = "not a template"
|
payload = "not a template"
|
||||||
payload_template = "a template"
|
payload_template = "a template"
|
||||||
self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, {
|
with pytest.raises(vol.Invalid):
|
||||||
mqtt.ATTR_TOPIC: "test/topic",
|
self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, {
|
||||||
mqtt.ATTR_PAYLOAD: payload,
|
mqtt.ATTR_TOPIC: "test/topic",
|
||||||
mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template
|
mqtt.ATTR_PAYLOAD: payload,
|
||||||
}, blocking=True)
|
mqtt.ATTR_PAYLOAD_TEMPLATE: payload_template
|
||||||
|
}, blocking=True)
|
||||||
assert not self.hass.data['mqtt'].async_publish.called
|
assert not self.hass.data['mqtt'].async_publish.called
|
||||||
|
|
||||||
def test_service_call_with_ascii_qos_retain_flags(self):
|
def test_service_call_with_ascii_qos_retain_flags(self):
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.notify as notify
|
import homeassistant.components.notify as notify
|
||||||
from homeassistant.setup import setup_component
|
from homeassistant.setup import setup_component
|
||||||
from homeassistant.components.notify import demo
|
from homeassistant.components.notify import demo
|
||||||
|
@ -81,7 +84,8 @@ class TestNotifyDemo(unittest.TestCase):
|
||||||
def test_sending_none_message(self):
|
def test_sending_none_message(self):
|
||||||
"""Test send with None as message."""
|
"""Test send with None as message."""
|
||||||
self._setup_notify()
|
self._setup_notify()
|
||||||
common.send_message(self.hass, None)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.send_message(self.hass, None)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
assert len(self.events) == 0
|
assert len(self.events) == 0
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,7 @@ class TestAlert(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up things to be run when tests are started."""
|
"""Set up things to be run when tests are started."""
|
||||||
self.hass = get_test_home_assistant()
|
self.hass = get_test_home_assistant()
|
||||||
|
self._setup_notify()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Stop everything that was started."""
|
"""Stop everything that was started."""
|
||||||
|
|
|
@ -6,6 +6,7 @@ from unittest.mock import patch
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import pytest
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import const
|
from homeassistant import const
|
||||||
from homeassistant.bootstrap import DATA_LOGGING
|
from homeassistant.bootstrap import DATA_LOGGING
|
||||||
|
@ -578,3 +579,29 @@ async def test_rendering_template_legacy_user(
|
||||||
json={"template": '{{ states.sensor.temperature.state }}'}
|
json={"template": '{{ states.sensor.temperature.state }}'}
|
||||||
)
|
)
|
||||||
assert resp.status == 401
|
assert resp.status == 401
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_call_service_not_found(hass, mock_api_client):
|
||||||
|
"""Test if the API failes 400 if unknown service."""
|
||||||
|
resp = await mock_api_client.post(
|
||||||
|
const.URL_API_SERVICES_SERVICE.format(
|
||||||
|
"test_domain", "test_service"))
|
||||||
|
assert resp.status == 400
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_call_service_bad_data(hass, mock_api_client):
|
||||||
|
"""Test if the API failes 400 if unknown service."""
|
||||||
|
test_value = []
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def listener(service_call):
|
||||||
|
"""Record that our service got called."""
|
||||||
|
test_value.append(1)
|
||||||
|
|
||||||
|
hass.services.async_register("test_domain", "test_service", listener,
|
||||||
|
schema=vol.Schema({'hello': str}))
|
||||||
|
|
||||||
|
resp = await mock_api_client.post(
|
||||||
|
const.URL_API_SERVICES_SERVICE.format(
|
||||||
|
"test_domain", "test_service"), json={'hello': 5})
|
||||||
|
assert resp.status == 400
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import CoreState, State, Context
|
from homeassistant.core import CoreState, State, Context
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components.input_datetime import (
|
from homeassistant.components.input_datetime import (
|
||||||
|
@ -109,10 +112,11 @@ def test_set_invalid(hass):
|
||||||
dt_obj = datetime.datetime(2017, 9, 7, 19, 46)
|
dt_obj = datetime.datetime(2017, 9, 7, 19, 46)
|
||||||
time_portion = dt_obj.time()
|
time_portion = dt_obj.time()
|
||||||
|
|
||||||
yield from hass.services.async_call('input_datetime', 'set_datetime', {
|
with pytest.raises(vol.Invalid):
|
||||||
'entity_id': 'test_date',
|
yield from hass.services.async_call('input_datetime', 'set_datetime', {
|
||||||
'time': time_portion
|
'entity_id': 'test_date',
|
||||||
})
|
'time': time_portion
|
||||||
|
})
|
||||||
yield from hass.async_block_till_done()
|
yield from hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
|
@ -4,6 +4,9 @@ import logging
|
||||||
from datetime import (timedelta, datetime)
|
from datetime import (timedelta, datetime)
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import sun
|
from homeassistant.components import sun
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -89,7 +92,9 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
calls.append(event)
|
calls.append(event)
|
||||||
|
|
||||||
self.hass.bus.listen(logbook.EVENT_LOGBOOK_ENTRY, event_listener)
|
self.hass.bus.listen(logbook.EVENT_LOGBOOK_ENTRY, event_listener)
|
||||||
self.hass.services.call(logbook.DOMAIN, 'log', {}, True)
|
|
||||||
|
with pytest.raises(vol.Invalid):
|
||||||
|
self.hass.services.call(logbook.DOMAIN, 'log', {}, True)
|
||||||
|
|
||||||
# Logbook entry service call results in firing an event.
|
# Logbook entry service call results in firing an event.
|
||||||
# Our service call will unblock when the event listeners have been
|
# Our service call will unblock when the event listeners have been
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.bootstrap import async_setup_component
|
from homeassistant.bootstrap import async_setup_component
|
||||||
from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA
|
from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA
|
||||||
import homeassistant.components.snips as snips
|
import homeassistant.components.snips as snips
|
||||||
|
@ -452,12 +455,11 @@ async def test_snips_say_invalid_config(hass, caplog):
|
||||||
snips.SERVICE_SCHEMA_SAY)
|
snips.SERVICE_SCHEMA_SAY)
|
||||||
|
|
||||||
data = {'text': 'Hello', 'badKey': 'boo'}
|
data = {'text': 'Hello', 'badKey': 'boo'}
|
||||||
await hass.services.async_call('snips', 'say', data)
|
with pytest.raises(vol.Invalid):
|
||||||
|
await hass.services.async_call('snips', 'say', data)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
assert 'ERROR' in caplog.text
|
|
||||||
assert 'Invalid service data' in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
async def test_snips_say_action_invalid(hass, caplog):
|
async def test_snips_say_action_invalid(hass, caplog):
|
||||||
|
@ -466,12 +468,12 @@ async def test_snips_say_action_invalid(hass, caplog):
|
||||||
snips.SERVICE_SCHEMA_SAY_ACTION)
|
snips.SERVICE_SCHEMA_SAY_ACTION)
|
||||||
|
|
||||||
data = {'text': 'Hello', 'can_be_enqueued': 'notabool'}
|
data = {'text': 'Hello', 'can_be_enqueued': 'notabool'}
|
||||||
await hass.services.async_call('snips', 'say_action', data)
|
|
||||||
|
with pytest.raises(vol.Invalid):
|
||||||
|
await hass.services.async_call('snips', 'say_action', data)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
assert 'ERROR' in caplog.text
|
|
||||||
assert 'Invalid service data' in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
async def test_snips_feedback_on(hass, caplog):
|
async def test_snips_feedback_on(hass, caplog):
|
||||||
|
@ -510,7 +512,8 @@ async def test_snips_feedback_config(hass, caplog):
|
||||||
snips.SERVICE_SCHEMA_FEEDBACK)
|
snips.SERVICE_SCHEMA_FEEDBACK)
|
||||||
|
|
||||||
data = {'site_id': 'remote', 'test': 'test'}
|
data = {'site_id': 'remote', 'test': 'test'}
|
||||||
await hass.services.async_call('snips', 'feedback_on', data)
|
with pytest.raises(vol.Invalid):
|
||||||
|
await hass.services.async_call('snips', 'feedback_on', data)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
|
@ -3,6 +3,7 @@ import asyncio
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components.wake_on_lan import (
|
from homeassistant.components.wake_on_lan import (
|
||||||
|
@ -34,10 +35,10 @@ def test_send_magic_packet(hass, caplog, mock_wakeonlan):
|
||||||
assert mock_wakeonlan.mock_calls[-1][1][0] == mac
|
assert mock_wakeonlan.mock_calls[-1][1][0] == mac
|
||||||
assert mock_wakeonlan.mock_calls[-1][2]['ip_address'] == bc_ip
|
assert mock_wakeonlan.mock_calls[-1][2]['ip_address'] == bc_ip
|
||||||
|
|
||||||
yield from hass.services.async_call(
|
with pytest.raises(vol.Invalid):
|
||||||
DOMAIN, SERVICE_SEND_MAGIC_PACKET,
|
yield from hass.services.async_call(
|
||||||
{"broadcast_address": bc_ip}, blocking=True)
|
DOMAIN, SERVICE_SEND_MAGIC_PACKET,
|
||||||
assert 'ERROR' in caplog.text
|
{"broadcast_address": bc_ip}, blocking=True)
|
||||||
assert len(mock_wakeonlan.mock_calls) == 1
|
assert len(mock_wakeonlan.mock_calls) == 1
|
||||||
|
|
||||||
yield from hass.services.async_call(
|
yield from hass.services.async_call(
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
"""The tests for the demo water_heater component."""
|
"""The tests for the demo water_heater component."""
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.util.unit_system import (
|
from homeassistant.util.unit_system import (
|
||||||
IMPERIAL_SYSTEM
|
IMPERIAL_SYSTEM
|
||||||
)
|
)
|
||||||
|
@ -48,7 +51,8 @@ class TestDemowater_heater(unittest.TestCase):
|
||||||
"""Test setting the target temperature without required attribute."""
|
"""Test setting the target temperature without required attribute."""
|
||||||
state = self.hass.states.get(ENTITY_WATER_HEATER)
|
state = self.hass.states.get(ENTITY_WATER_HEATER)
|
||||||
assert 119 == state.attributes.get('temperature')
|
assert 119 == state.attributes.get('temperature')
|
||||||
common.set_temperature(self.hass, None, ENTITY_WATER_HEATER)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_temperature(self.hass, None, ENTITY_WATER_HEATER)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
assert 119 == state.attributes.get('temperature')
|
assert 119 == state.attributes.get('temperature')
|
||||||
|
|
||||||
|
@ -69,7 +73,8 @@ class TestDemowater_heater(unittest.TestCase):
|
||||||
state = self.hass.states.get(ENTITY_WATER_HEATER)
|
state = self.hass.states.get(ENTITY_WATER_HEATER)
|
||||||
assert "eco" == state.attributes.get('operation_mode')
|
assert "eco" == state.attributes.get('operation_mode')
|
||||||
assert "eco" == state.state
|
assert "eco" == state.state
|
||||||
common.set_operation_mode(self.hass, None, ENTITY_WATER_HEATER)
|
with pytest.raises(vol.Invalid):
|
||||||
|
common.set_operation_mode(self.hass, None, ENTITY_WATER_HEATER)
|
||||||
self.hass.block_till_done()
|
self.hass.block_till_done()
|
||||||
state = self.hass.states.get(ENTITY_WATER_HEATER)
|
state = self.hass.states.get(ENTITY_WATER_HEATER)
|
||||||
assert "eco" == state.attributes.get('operation_mode')
|
assert "eco" == state.attributes.get('operation_mode')
|
||||||
|
|
|
@ -49,6 +49,25 @@ async def test_call_service(hass, websocket_client):
|
||||||
assert call.data == {'hello': 'world'}
|
assert call.data == {'hello': 'world'}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_call_service_not_found(hass, websocket_client):
|
||||||
|
"""Test call service command."""
|
||||||
|
await websocket_client.send_json({
|
||||||
|
'id': 5,
|
||||||
|
'type': commands.TYPE_CALL_SERVICE,
|
||||||
|
'domain': 'domain_test',
|
||||||
|
'service': 'test_service',
|
||||||
|
'service_data': {
|
||||||
|
'hello': 'world'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
assert msg['id'] == 5
|
||||||
|
assert msg['type'] == const.TYPE_RESULT
|
||||||
|
assert not msg['success']
|
||||||
|
assert msg['error']['code'] == const.ERR_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
async def test_subscribe_unsubscribe_events(hass, websocket_client):
|
async def test_subscribe_unsubscribe_events(hass, websocket_client):
|
||||||
"""Test subscribe/unsubscribe events command."""
|
"""Test subscribe/unsubscribe events command."""
|
||||||
init_count = sum(hass.bus.async_listeners().values())
|
init_count = sum(hass.bus.async_listeners().values())
|
||||||
|
|
|
@ -947,7 +947,7 @@ class TestZWaveServices(unittest.TestCase):
|
||||||
assert self.zwave_network.stop.called
|
assert self.zwave_network.stop.called
|
||||||
assert len(self.zwave_network.stop.mock_calls) == 1
|
assert len(self.zwave_network.stop.mock_calls) == 1
|
||||||
assert mock_fire.called
|
assert mock_fire.called
|
||||||
assert len(mock_fire.mock_calls) == 2
|
assert len(mock_fire.mock_calls) == 1
|
||||||
assert mock_fire.mock_calls[0][1][0] == const.EVENT_NETWORK_STOP
|
assert mock_fire.mock_calls[0][1][0] == const.EVENT_NETWORK_STOP
|
||||||
|
|
||||||
def test_rename_node(self):
|
def test_rename_node(self):
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||||
__version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM,
|
__version__, EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, CONF_UNIT_SYSTEM,
|
||||||
ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS,
|
ATTR_NOW, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS,
|
||||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE,
|
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_CLOSE,
|
||||||
EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_SERVICE_EXECUTED)
|
EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED)
|
||||||
|
|
||||||
from tests.common import get_test_home_assistant, async_mock_service
|
from tests.common import get_test_home_assistant, async_mock_service
|
||||||
|
|
||||||
|
@ -673,13 +673,8 @@ class TestServiceRegistry(unittest.TestCase):
|
||||||
|
|
||||||
def test_call_non_existing_with_blocking(self):
|
def test_call_non_existing_with_blocking(self):
|
||||||
"""Test non-existing with blocking."""
|
"""Test non-existing with blocking."""
|
||||||
prior = ha.SERVICE_CALL_LIMIT
|
with pytest.raises(ha.ServiceNotFound):
|
||||||
try:
|
self.services.call('test_domain', 'i_do_not_exist', blocking=True)
|
||||||
ha.SERVICE_CALL_LIMIT = 0.01
|
|
||||||
assert not self.services.call('test_domain', 'i_do_not_exist',
|
|
||||||
blocking=True)
|
|
||||||
finally:
|
|
||||||
ha.SERVICE_CALL_LIMIT = prior
|
|
||||||
|
|
||||||
def test_async_service(self):
|
def test_async_service(self):
|
||||||
"""Test registering and calling an async service."""
|
"""Test registering and calling an async service."""
|
||||||
|
@ -1005,4 +1000,3 @@ async def test_service_executed_with_subservices(hass):
|
||||||
assert len(calls) == 4
|
assert len(calls) == 4
|
||||||
assert [call.service for call in calls] == [
|
assert [call.service for call in calls] == [
|
||||||
'outer', 'inner', 'inner', 'outer']
|
'outer', 'inner', 'inner', 'outer']
|
||||||
assert len(hass.bus.async_listeners().get(EVENT_SERVICE_EXECUTED, [])) == 0
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue