Skip to content

Commit

Permalink
Arcam config flow (home-assistant#34384)
Browse files Browse the repository at this point in the history
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
  • Loading branch information
3 people authored Jun 6, 2020
1 parent 524b48b commit 31973de
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 203 deletions.
89 changes: 7 additions & 82 deletions homeassistant/components/arcam_fmj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,15 @@
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
import async_timeout
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_ZONE,
EVENT_HOMEASSISTANT_STOP,
SERVICE_TURN_ON,
)
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, HomeAssistantType

from .const import (
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
DOMAIN_DATA_CONFIG,
DOMAIN_DATA_ENTRIES,
DOMAIN_DATA_TASKS,
SIGNAL_CLIENT_DATA,
Expand All @@ -35,44 +23,7 @@

_LOGGER = logging.getLogger(__name__)


def _optional_zone(value):
if value:
return ZONE_SCHEMA(value)
return ZONE_SCHEMA({})


def _zone_name_validator(config):
for zone, zone_config in config[CONF_ZONE].items():
if CONF_NAME not in zone_config:
zone_config[
CONF_NAME
] = f"{DEFAULT_NAME} ({config[CONF_HOST]}:{config[CONF_PORT]}) - {zone}"
return config


ZONE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(SERVICE_TURN_ON): cv.SERVICE_SCHEMA,
}
)

DEVICE_SCHEMA = vol.Schema(
vol.All(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
vol.Optional(CONF_ZONE, default={1: _optional_zone(None)}): {
vol.In([1, 2]): _optional_zone
},
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
},
_zone_name_validator,
)
)
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115")


async def _await_cancel(task):
Expand All @@ -83,27 +34,10 @@ async def _await_cancel(task):
pass


CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA])}, extra=vol.ALLOW_EXTRA
)


async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the component."""
hass.data[DOMAIN_DATA_ENTRIES] = {}
hass.data[DOMAIN_DATA_TASKS] = {}
hass.data[DOMAIN_DATA_CONFIG] = {}

for device in config[DOMAIN]:
hass.data[DOMAIN_DATA_CONFIG][(device[CONF_HOST], device[CONF_PORT])] = device

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={CONF_HOST: device[CONF_HOST], CONF_PORT: device[CONF_PORT]},
)
)

async def _stop(_):
asyncio.gather(
Expand All @@ -116,21 +50,12 @@ async def _stop(_):


async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry):
"""Set up an access point from a config entry."""
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
"""Set up config entry."""
entries = hass.data[DOMAIN_DATA_ENTRIES]
tasks = hass.data[DOMAIN_DATA_TASKS]

config = hass.data[DOMAIN_DATA_CONFIG].get(
(entry.data[CONF_HOST], entry.data[CONF_PORT]),
DEVICE_SCHEMA(
{CONF_HOST: entry.data[CONF_HOST], CONF_PORT: entry.data[CONF_PORT]}
),
)
tasks = hass.data.setdefault(DOMAIN_DATA_TASKS, {})

hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = {
"client": client,
"config": config,
}
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries[entry.entry_id] = client

task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL))
tasks[entry.entry_id] = task
Expand Down
99 changes: 87 additions & 12 deletions homeassistant/components/arcam_fmj/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,102 @@
"""Config flow to configure the Arcam FMJ component."""
from operator import itemgetter
import logging
from urllib.parse import urlparse

from arcam.fmj.client import Client, ConnectionFailed
from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_UDN
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES

from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)

_GETKEY = itemgetter(CONF_HOST, CONF_PORT)

def get_entry_client(hass, entry):
"""Retrieve client associated with a config entry."""
return hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id]


@config_entries.HANDLERS.register(DOMAIN)
class ArcamFmjFlowHandler(config_entries.ConfigFlow):
"""Handle a SimpliSafe config flow."""
"""Handle config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
entries = self.hass.config_entries.async_entries(DOMAIN)
import_key = _GETKEY(import_config)
for entry in entries:
if _GETKEY(entry.data) == import_key:
return self.async_abort(reason="already_setup")
async def _async_set_unique_id_and_update(self, host, port, uuid):
await self.async_set_unique_id(uuid)
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})

async def _async_check_and_create(self, host, port):
client = Client(host, port)
try:
await client.start()
except ConnectionFailed:
return self.async_abort(reason="unable_to_connect")
finally:
await client.stop()

return self.async_create_entry(
title=f"{DEFAULT_NAME} ({host})", data={CONF_HOST: host, CONF_PORT: port},
)

async def async_step_user(self, user_info=None):
"""Handle a discovered device."""
errors = {}

if user_info is not None:
uuid = await get_uniqueid_from_host(
async_get_clientsession(self.hass), user_info[CONF_HOST]
)
if uuid:
await self._async_set_unique_id_and_update(
user_info[CONF_HOST], user_info[CONF_PORT], uuid
)

return await self._async_check_and_create(
user_info[CONF_HOST], user_info[CONF_PORT]
)

fields = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}

return self.async_show_form(
step_id="user", data_schema=vol.Schema(fields), errors=errors
)

async def async_step_confirm(self, user_input=None):
"""Handle user-confirmation of discovered node."""
context = self.context # pylint: disable=no-member
placeholders = {
"host": context[CONF_HOST],
}
context["title_placeholders"] = placeholders

if user_input is not None:
return await self._async_check_and_create(
context[CONF_HOST], context[CONF_PORT]
)

return self.async_show_form(
step_id="confirm", description_placeholders=placeholders
)

async def async_step_ssdp(self, discovery_info):
"""Handle a discovered device."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
port = DEFAULT_PORT
uuid = get_uniqueid_from_udn(discovery_info[ATTR_UPNP_UDN])

await self._async_set_unique_id_and_update(host, port, uuid)

return self.async_create_entry(title="Arcam FMJ", data=import_config)
context = self.context # pylint: disable=no-member
context[CONF_HOST] = host
context[CONF_PORT] = DEFAULT_PORT
return await self.async_step_confirm()
1 change: 0 additions & 1 deletion homeassistant/components/arcam_fmj/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@

DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"
DOMAIN_DATA_TASKS = f"{DOMAIN}.tasks"
DOMAIN_DATA_CONFIG = f"{DOMAIN}.config"
10 changes: 8 additions & 2 deletions homeassistant/components/arcam_fmj/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
{
"domain": "arcam_fmj",
"name": "Arcam FMJ Receivers",
"config_flow": false,
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"requirements": ["arcam-fmj==0.4.6"],
"requirements": ["arcam-fmj==0.5.1"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
"manufacturer": "ARCAM"
}
],
"codeowners": ["@elupus"]
}
53 changes: 21 additions & 32 deletions homeassistant/components/arcam_fmj/media_player.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Arcam media player."""
import logging
from typing import Optional

from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes
from arcam.fmj.state import State
Expand All @@ -17,21 +16,13 @@
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_NAME,
CONF_ZONE,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.service import async_call_from_config
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.typing import HomeAssistantType

from .config_flow import get_entry_client
from .const import (
DOMAIN,
DOMAIN_DATA_ENTRIES,
EVENT_TURN_ON,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
Expand All @@ -47,19 +38,17 @@ async def async_setup_entry(
async_add_entities,
):
"""Set up the configuration entry."""
data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id]
client = data["client"]
config = data["config"]

client = get_entry_client(hass, config_entry)

async_add_entities(
[
ArcamFmj(
config_entry.title,
State(client, zone),
config_entry.unique_id or config_entry.entry_id,
zone_config[CONF_NAME],
zone_config.get(SERVICE_TURN_ON),
)
for zone, zone_config in config[CONF_ZONE].items()
for zone in [1, 2]
],
True,
)
Expand All @@ -71,13 +60,13 @@ class ArcamFmj(MediaPlayerEntity):
"""Representation of a media device."""

def __init__(
self, state: State, uuid: str, name: str, turn_on: Optional[ConfigType]
self, device_name, state: State, uuid: str,
):
"""Initialize device."""
self._state = state
self._device_name = device_name
self._name = f"{device_name} - Zone: {state.zn}"
self._uuid = uuid
self._name = name
self._turn_on = turn_on
self._support = (
SUPPORT_SELECT_SOURCE
| SUPPORT_VOLUME_SET
Expand All @@ -102,6 +91,11 @@ def _get_2ch(self):
)
)

@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self._state.zn == 1

@property
def unique_id(self):
"""Return unique identifier if known."""
Expand All @@ -111,8 +105,12 @@ def unique_id(self):
def device_info(self):
"""Return a device description for device registry."""
return {
"identifiers": {(DOMAIN, self._state.client.host, self._state.client.port)},
"model": "FMJ",
"name": self._device_name,
"identifiers": {
(DOMAIN, self._uuid),
(DOMAIN, self._state.client.host, self._state.client.port),
},
"model": "Arcam FMJ AVR",
"manufacturer": "Arcam",
}

Expand Down Expand Up @@ -229,15 +227,6 @@ async def async_turn_on(self):
if self._state.get_power() is not None:
_LOGGER.debug("Turning on device using connection")
await self._state.set_power(True)
elif self._turn_on:
_LOGGER.debug("Turning on device using service call")
await async_call_from_config(
self.hass,
self._turn_on,
variables=None,
blocking=True,
validate_config=False,
)
else:
_LOGGER.debug("Firing event to turn on device")
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
Expand Down
Loading

0 comments on commit 31973de

Please sign in to comment.