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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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:
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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