Add HEOS sign-in/out services (#23729)

* Add HEOS sign-in/out services

* Fix typo in comment
pull/23746/head
Andrew Sayre 2019-05-07 11:39:42 -05:00 committed by Paulus Schoutsen
parent 102beaa044
commit 02d8731a61
9 changed files with 220 additions and 4 deletions

View File

@ -15,6 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import Throttle
from . import services
from .config_flow import format_title
from .const import (
COMMAND_RETRY_ATTEMPTS, COMMAND_RETRY_DELAY, DATA_CONTROLLER_MANAGER,
@ -81,8 +82,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
if controller.is_signed_in:
favorites = await controller.get_favorites()
else:
_LOGGER.warning("%s is not logged in to your HEOS account and will"
" be unable to retrieve your favorites", host)
_LOGGER.warning(
"%s is not logged in to a HEOS account and will be unable "
"to retrieve HEOS favorites: Use the 'heos.sign_in' service "
"to sign-in to a HEOS account", host)
inputs = await controller.get_input_sources()
except (asyncio.TimeoutError, ConnectionError, CommandError) as error:
await controller.disconnect()
@ -101,6 +104,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
DATA_SOURCE_MANAGER: source_manager,
MEDIA_PLAYER_DOMAIN: players
}
services.register(hass, controller)
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, MEDIA_PLAYER_DOMAIN))
return True
@ -111,6 +117,9 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry):
controller_manager = hass.data[DOMAIN][DATA_CONTROLLER_MANAGER]
await controller_manager.disconnect()
hass.data.pop(DOMAIN)
services.remove(hass)
return await hass.config_entries.async_forward_entry_unload(
entry, MEDIA_PLAYER_DOMAIN)

View File

@ -1,9 +1,13 @@
"""Const for the HEOS integration."""
ATTR_PASSWORD = "password"
ATTR_USERNAME = "username"
COMMAND_RETRY_ATTEMPTS = 2
COMMAND_RETRY_DELAY = 1
DATA_CONTROLLER_MANAGER = "controller"
DATA_SOURCE_MANAGER = "source_manager"
DATA_DISCOVERED_HOSTS = "heos_discovered_hosts"
DOMAIN = 'heos'
SERVICE_SIGN_IN = "sign_in"
SERVICE_SIGN_OUT = "sign_out"
SIGNAL_HEOS_UPDATED = "heos_updated"

View File

@ -0,0 +1,66 @@
"""Services for the HEOS integration."""
import asyncio
import functools
import logging
from pyheos import CommandError, Heos, const
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from .const import (
ATTR_PASSWORD, ATTR_USERNAME, DOMAIN, SERVICE_SIGN_IN, SERVICE_SIGN_OUT)
_LOGGER = logging.getLogger(__name__)
HEOS_SIGN_IN_SCHEMA = vol.Schema({
vol.Required(ATTR_USERNAME): cv.string,
vol.Required(ATTR_PASSWORD): cv.string
})
HEOS_SIGN_OUT_SCHEMA = vol.Schema({})
def register(hass: HomeAssistantType, controller: Heos):
"""Register HEOS services."""
hass.services.async_register(
DOMAIN, SERVICE_SIGN_IN,
functools.partial(_sign_in_handler, controller),
schema=HEOS_SIGN_IN_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_SIGN_OUT,
functools.partial(_sign_out_handler, controller),
schema=HEOS_SIGN_OUT_SCHEMA)
def remove(hass: HomeAssistantType):
"""Unregister HEOS services."""
hass.services.async_remove(DOMAIN, SERVICE_SIGN_IN)
hass.services.async_remove(DOMAIN, SERVICE_SIGN_OUT)
async def _sign_in_handler(controller, service):
"""Sign in to the HEOS account."""
if controller.connection_state != const.STATE_CONNECTED:
_LOGGER.error("Unable to sign in because HEOS is not connected")
return
username = service.data[ATTR_USERNAME]
password = service.data[ATTR_PASSWORD]
try:
await controller.sign_in(username, password)
except CommandError as err:
_LOGGER.error("Sign in failed: %s", err)
except (asyncio.TimeoutError, ConnectionError) as err:
_LOGGER.error("Unable to sign in: %s", err)
async def _sign_out_handler(controller, service):
"""Sign out of the HEOS account."""
if controller.connection_state != const.STATE_CONNECTED:
_LOGGER.error("Unable to sign out because HEOS is not connected")
return
try:
await controller.sign_out()
except (asyncio.TimeoutError, ConnectionError, CommandError) as err:
_LOGGER.error("Unable to sign out: %s", err)

View File

@ -0,0 +1,12 @@
sign_in:
description: Sign the controller in to a HEOS account.
fields:
username:
description: The username or email of the HEOS account. [Required]
example: 'example@example.com'
password:
description: The password of the HEOS account. [Required]
example: 'password'
sign_out:
description: Sign the controller out of the HEOS account.

View File

@ -936,6 +936,9 @@ class Service:
"""Initialize a service."""
self.func = func
self.schema = schema
# Properly detect wrapped functions
while isinstance(func, functools.partial):
func = func.func
self.is_callback = is_callback(func)
self.is_coroutinefunction = asyncio.iscoroutinefunction(func)

View File

@ -35,6 +35,7 @@ def controller_fixture(
mock_heos.load_players.return_value = change_data
mock_heos.is_signed_in = True
mock_heos.signed_in_username = "user@user.com"
mock_heos.connection_state = const.STATE_CONNECTED
yield mock_heos

View File

@ -102,8 +102,9 @@ async def test_async_setup_entry_not_signed_in_loads_platforms(
assert hass.data[DOMAIN][MEDIA_PLAYER_DOMAIN] == controller.players
assert hass.data[DOMAIN][DATA_SOURCE_MANAGER].favorites == {}
assert hass.data[DOMAIN][DATA_SOURCE_MANAGER].inputs == input_sources
assert "127.0.0.1 is not logged in to your HEOS account and will be " \
"unable to retrieve your favorites" in caplog.text
assert "127.0.0.1 is not logged in to a HEOS account and will be unable " \
"to retrieve HEOS favorites: Use the 'heos.sign_in' service to " \
"sign-in to a HEOS account" in caplog.text
async def test_async_setup_entry_connect_failure(

View File

@ -0,0 +1,98 @@
"""Tests for the services module."""
from pyheos import CommandError, const
from homeassistant.components.heos.const import (
ATTR_PASSWORD, ATTR_USERNAME, DOMAIN, SERVICE_SIGN_IN, SERVICE_SIGN_OUT)
from homeassistant.setup import async_setup_component
async def setup_component(hass, config_entry):
"""Set up the component for testing."""
config_entry.add_to_hass(hass)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
async def test_sign_in(hass, config_entry, controller):
"""Test the sign-in service."""
await setup_component(hass, config_entry)
await hass.services.async_call(
DOMAIN, SERVICE_SIGN_IN,
{ATTR_USERNAME: "test@test.com", ATTR_PASSWORD: "password"},
blocking=True)
controller.sign_in.assert_called_once_with("test@test.com", "password")
async def test_sign_in_not_connected(hass, config_entry, controller, caplog):
"""Test sign-in service logs error when not connected."""
await setup_component(hass, config_entry)
controller.connection_state = const.STATE_RECONNECTING
await hass.services.async_call(
DOMAIN, SERVICE_SIGN_IN,
{ATTR_USERNAME: "test@test.com", ATTR_PASSWORD: "password"},
blocking=True)
assert controller.sign_in.call_count == 0
assert "Unable to sign in because HEOS is not connected" in caplog.text
async def test_sign_in_failed(hass, config_entry, controller, caplog):
"""Test sign-in service logs error when not connected."""
await setup_component(hass, config_entry)
controller.sign_in.side_effect = CommandError("", "Invalid credentials", 6)
await hass.services.async_call(
DOMAIN, SERVICE_SIGN_IN,
{ATTR_USERNAME: "test@test.com", ATTR_PASSWORD: "password"},
blocking=True)
controller.sign_in.assert_called_once_with("test@test.com", "password")
assert "Sign in failed: Invalid credentials (6)" in caplog.text
async def test_sign_in_unknown_error(hass, config_entry, controller, caplog):
"""Test sign-in service logs error for failure."""
await setup_component(hass, config_entry)
controller.sign_in.side_effect = ConnectionError
await hass.services.async_call(
DOMAIN, SERVICE_SIGN_IN,
{ATTR_USERNAME: "test@test.com", ATTR_PASSWORD: "password"},
blocking=True)
controller.sign_in.assert_called_once_with("test@test.com", "password")
assert "Unable to sign in" in caplog.text
async def test_sign_out(hass, config_entry, controller):
"""Test the sign-out service."""
await setup_component(hass, config_entry)
await hass.services.async_call(DOMAIN, SERVICE_SIGN_OUT, {}, blocking=True)
assert controller.sign_out.call_count == 1
async def test_sign_out_not_connected(hass, config_entry, controller, caplog):
"""Test the sign-out service."""
await setup_component(hass, config_entry)
controller.connection_state = const.STATE_RECONNECTING
await hass.services.async_call(DOMAIN, SERVICE_SIGN_OUT, {}, blocking=True)
assert controller.sign_out.call_count == 0
assert "Unable to sign out because HEOS is not connected" in caplog.text
async def test_sign_out_unknown_error(hass, config_entry, controller, caplog):
"""Test the sign-out service."""
await setup_component(hass, config_entry)
controller.sign_out.side_effect = ConnectionError
await hass.services.async_call(DOMAIN, SERVICE_SIGN_OUT, {}, blocking=True)
assert controller.sign_out.call_count == 1
assert "Unable to sign out" in caplog.text

View File

@ -744,6 +744,28 @@ class TestServiceRegistry(unittest.TestCase):
self.hass.block_till_done()
assert 1 == len(calls)
def test_async_service_partial(self):
"""Test registering and calling an wrapped async service."""
calls = []
async def service_handler(call):
"""Service handler coroutine."""
calls.append(call)
self.services.register(
'test_domain', 'register_calls',
functools.partial(service_handler))
self.hass.block_till_done()
assert len(self.calls_register) == 1
assert self.calls_register[-1].data['domain'] == 'test_domain'
assert self.calls_register[-1].data['service'] == 'register_calls'
assert self.services.call('test_domain', 'REGISTER_CALLS',
blocking=True)
self.hass.block_till_done()
assert len(calls) == 1
def test_callback_service(self):
"""Test registering and calling an async service."""
calls = []