Add HEOS sign-in/out services (#23729)
* Add HEOS sign-in/out services * Fix typo in commentpull/23746/head
parent
102beaa044
commit
02d8731a61
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -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.
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue