Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for arcam fmj receivers (#24621)
* 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
1 parent
f90fe7e
commit ab832cd
Showing
12 changed files
with
644 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"config": { | ||
"title": "Arcam FMJ", | ||
"step": {}, | ||
"error": {}, | ||
"abort": {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
] | ||
} |
Oops, something went wrong.