Skip to content

Commit

Permalink
Add support for arcam fmj receivers (#24621)
Browse files Browse the repository at this point in the history
* Add arcam_fmj support

* Just use use state in player avoid direct client access

* Avoid leaking exceptions on invalid data

* Fix return value for volume in case of 0

* Mark component as having no coverage

* Add new requirement

* Add myself as maintainer

* Correct linting errors

* Use async_create_task instead of async_add_job

* Use new style string format instead of concat

* Don't call init of base class without init

* Annotate callbacks with @callback

Otherwise they won't be called in loop

* Reduce log level to debug

* Use async_timeout instead of wait_for

* Bump to version of arcam_fmj supporting 3.5

* Fix extra spaces

* Drop somewhat flaky unique_id

* Un-blackify ident to satisy pylint

* Un-blackify ident to satisy pylint

* Move default name calculation to config validation

* Add test folder

* Drop unused code

* Add tests for config flow import
  • Loading branch information
elupus authored and MartinHjelmare committed Jul 8, 2019
1 parent f90fe7e commit ab832cd
Show file tree
Hide file tree
Showing 12 changed files with 644 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Expand Up @@ -38,6 +38,8 @@ omit =
homeassistant/components/apple_tv/*
homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/media_player.py
homeassistant/components/arcam_fmj/__init__.py
homeassistant/components/arduino/*
homeassistant/components/arest/binary_sensor.py
homeassistant/components/arest/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -26,6 +26,7 @@ homeassistant/components/ambiclimate/* @danielhiversen
homeassistant/components/ambient_station/* @bachya
homeassistant/components/api/* @home-assistant/core
homeassistant/components/aprs/* @PhilRW
homeassistant/components/arcam_fmj/* @elupus
homeassistant/components/arduino/* @fabaff
homeassistant/components/arest/* @fabaff
homeassistant/components/asuswrt/* @kennedyshead
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/arcam_fmj/.translations/en.json
@@ -0,0 +1,8 @@
{
"config": {
"title": "Arcam FMJ",
"step": {},
"error": {},
"abort": {}
}
}
176 changes: 176 additions & 0 deletions homeassistant/components/arcam_fmj/__init__.py
@@ -0,0 +1,176 @@
"""Arcam component."""
import logging
import asyncio

import voluptuous as vol
import async_timeout
from arcam.fmj.client import Client
from arcam.fmj import ConnectionFailed

from homeassistant import config_entries
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
CONF_HOST,
CONF_NAME,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_ZONE,
SERVICE_TURN_ON,
)
from .const import (
DOMAIN,
DOMAIN_DATA_ENTRIES,
DOMAIN_DATA_CONFIG,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
)

_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] = "{} ({}:{}) - {}".format(
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 = 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_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],
},
)
)

return True


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

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

hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = {
"client": client,
"config": config,
}

asyncio.ensure_future(
_run_client(hass, client, config[CONF_SCAN_INTERVAL])
)

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "media_player")
)

return True


async def _run_client(hass, client, interval):
task = asyncio.Task.current_task()
run = True

async def _stop(_):
nonlocal run
run = False
task.cancel()
await task

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop)

def _listen(_):
hass.helpers.dispatcher.async_dispatcher_send(
SIGNAL_CLIENT_DATA, client.host
)

while run:
try:
with async_timeout.timeout(interval):
await client.start()

_LOGGER.debug("Client connected %s", client.host)
hass.helpers.dispatcher.async_dispatcher_send(
SIGNAL_CLIENT_STARTED, client.host
)

try:
with client.listen(_listen):
await client.process()
finally:
await client.stop()

_LOGGER.debug("Client disconnected %s", client.host)
hass.helpers.dispatcher.async_dispatcher_send(
SIGNAL_CLIENT_STOPPED, client.host
)

except ConnectionFailed:
await asyncio.sleep(interval)
except asyncio.TimeoutError:
continue
27 changes: 27 additions & 0 deletions homeassistant/components/arcam_fmj/config_flow.py
@@ -0,0 +1,27 @@
"""Config flow to configure the Arcam FMJ component."""
from operator import itemgetter

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT

from .const import DOMAIN

_GETKEY = itemgetter(CONF_HOST, CONF_PORT)


@config_entries.HANDLERS.register(DOMAIN)
class ArcamFmjFlowHandler(config_entries.ConfigFlow):
"""Handle a SimpliSafe 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")

return self.async_create_entry(title="Arcam FMJ", data=import_config)
13 changes: 13 additions & 0 deletions homeassistant/components/arcam_fmj/const.py
@@ -0,0 +1,13 @@
"""Constants used for arcam."""
DOMAIN = "arcam_fmj"

SIGNAL_CLIENT_STARTED = "arcam.client_started"
SIGNAL_CLIENT_STOPPED = "arcam.client_stopped"
SIGNAL_CLIENT_DATA = "arcam.client_data"

DEFAULT_PORT = 50000
DEFAULT_NAME = "Arcam FMJ"
DEFAULT_SCAN_INTERVAL = 5

DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN)
DOMAIN_DATA_CONFIG = "{}.config".format(DOMAIN)
13 changes: 13 additions & 0 deletions homeassistant/components/arcam_fmj/manifest.json
@@ -0,0 +1,13 @@
{
"domain": "arcam_fmj",
"name": "Arcam FMJ Receiver control",
"config_flow": false,
"documentation": "https://www.home-assistant.io/components/arcam_fmj",
"requirements": [
"arcam-fmj==0.4.3"
],
"dependencies": [],
"codeowners": [
"@elupus"
]
}

0 comments on commit ab832cd

Please sign in to comment.