Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for arcam fmj receivers #24621

Merged
merged 23 commits into from Jul 8, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -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
@@ -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
@@ -0,0 +1,8 @@
{
"config": {
"title": "Arcam FMJ",
"step": {},
"error": {},
"abort": {}
}
}
@@ -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:
This conversation was marked as resolved by MartinHjelmare

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 20, 2019

Member

Maybe we can run until task.done()?

Suggested change
while run:
while not task.done():

This comment has been minimized.

Copy link
@elupus

elupus Jun 20, 2019

Author Contributor

Sadly no. (would have been not task.cancelled()) There is a bug in aionursery used by the lib: agronholm/aionursery#1 which consumes the cancel exception. So task doesn't properly propagate the exception and thus doesn't get cancelled.

I'll see if i can get rid of aionursery use at some point if it's not fixed.

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
@@ -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):

This comment has been minimized.

Copy link
@frenck

frenck Jun 19, 2019

Member

So why only the import? That seems a bit inconvenient for the user?

This comment has been minimized.

Copy link
@elupus

elupus Jun 20, 2019

Author Contributor

Because without configuring a turn_on service call with an IR blaster or similar it's quite limited in use. And there is no good way to do that from config flows at the moment. Perhaps once option flows are in place...

"""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)
@@ -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)
@@ -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"
]
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.