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 8 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,7 @@ omit =
homeassistant/components/apple_tv/*
homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/*

This comment has been minimized.

Copy link
@frenck

frenck Jun 19, 2019

Member

Make sure you don't exclude the tests for your config flow...

This comment has been minimized.

Copy link
@elupus

elupus Jul 7, 2019

Author Contributor

As noted below, there is not real config flow. Only import. Since device registry is non working anyway for this integration (can't get unique id) i could just drop it and move back to old way?

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jul 7, 2019

Member

We should still test the config flow. That module needs 100% coverage.

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,160 @@
"""Arcam component."""
import logging
import asyncio

import voluptuous as vol
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_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({})


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

DEVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
vol.Optional(CONF_ZONE): {vol.In([1, 2]): _optional_zone},
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
}
)

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_add_job(
This conversation was marked as resolved by elupus

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 20, 2019

Member
Suggested change
hass.async_add_job(
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_add_job(
This conversation was marked as resolved by elupus

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 20, 2019

Member
Suggested change
hass.async_add_job(
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:
await asyncio.wait_for(client.start(), timeout=interval)
This conversation was marked as resolved by elupus

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 20, 2019

Member

We can use async_timeout:

Suggested change
await asyncio.wait_for(client.start(), timeout=interval)
with async_timeout.timeout(interval):
await client.start()

_LOGGER.info("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.info("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,34 @@
"""Config flow to configure the SimpliSafe component."""
from collections import OrderedDict
from operator import itemgetter
import logging

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

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)
_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

def __init__(self):
"""Initialize the config flow."""
self._config = OrderedDict()

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 = DOMAIN + ".entries"
This conversation was marked as resolved by elupus

This comment has been minimized.

Copy link
@MartinHjelmare

MartinHjelmare Jun 20, 2019

Member

Use new style string formatting instead of string concatenation.

DOMAIN_DATA_CONFIG = DOMAIN + ".config"
@@ -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.2"
],
"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.