From 0ec5b8add007878ae3c535967ba9619ce1c0ac3d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 10 Jun 2019 13:37:38 +0200 Subject: [PATCH 01/23] Add arcam_fmj support --- .../arcam_fmj/.translations/en.json | 8 + .../components/arcam_fmj/__init__.py | 158 ++++++++ .../components/arcam_fmj/config_flow.py | 34 ++ homeassistant/components/arcam_fmj/const.py | 13 + .../components/arcam_fmj/manifest.json | 11 + .../components/arcam_fmj/media_player.py | 344 ++++++++++++++++++ .../components/arcam_fmj/strings.json | 8 + 7 files changed, 576 insertions(+) create mode 100644 homeassistant/components/arcam_fmj/.translations/en.json create mode 100644 homeassistant/components/arcam_fmj/__init__.py create mode 100644 homeassistant/components/arcam_fmj/config_flow.py create mode 100644 homeassistant/components/arcam_fmj/const.py create mode 100644 homeassistant/components/arcam_fmj/manifest.json create mode 100644 homeassistant/components/arcam_fmj/media_player.py create mode 100644 homeassistant/components/arcam_fmj/strings.json diff --git a/homeassistant/components/arcam_fmj/.translations/en.json b/homeassistant/components/arcam_fmj/.translations/en.json new file mode 100644 index 00000000000000..5844c277364d04 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/en.json @@ -0,0 +1,8 @@ +{ + "config": { + "title": "Arcam FMJ", + "step": {}, + "error": {}, + "abort": {} + } +} diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py new file mode 100644 index 00000000000000..99b31030a074dc --- /dev/null +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -0,0 +1,158 @@ +"""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, + STATE_OFF, + STATE_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( + 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( + 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: + await asyncio.wait_for(client.start(), timeout=interval) + + _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 diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py new file mode 100644 index 00000000000000..7cabbc7e318bb7 --- /dev/null +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -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): + """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) diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py new file mode 100644 index 00000000000000..8df6746338a069 --- /dev/null +++ b/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 = DOMAIN + ".entries" +DOMAIN_DATA_CONFIG = DOMAIN + ".config" diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json new file mode 100644 index 00000000000000..7cbd91100910c4 --- /dev/null +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -0,0 +1,11 @@ +{ + "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": [] +} diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py new file mode 100644 index 00000000000000..5ba8d527571f58 --- /dev/null +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -0,0 +1,344 @@ +"""Arcam media player""" +import logging +from typing import Optional + +from arcam.fmj import ( + DecodeMode2CH, + DecodeModeMCH, + IncomingAudioFormat, + SourceCodes, +) +from arcam.fmj.client import Client +from arcam.fmj.state import State + +from homeassistant import config_entries +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, + SUPPORT_SELECT_SOUND_MODE, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_ON, + SUPPORT_TURN_OFF, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.const import ( + CONF_NAME, + CONF_ZONE, + CONF_HOST, + CONF_PORT, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.service import async_call_from_config + +from .const import ( + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, + DEFAULT_NAME, + DOMAIN_DATA_ENTRIES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: config_entries.ConfigEntry, + async_add_entities, +): + """Setup platform.""" + data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id] + client = data["client"] + config = data["config"] + + host = config[CONF_HOST] + port = config[CONF_PORT] + + async_add_entities( + [ + ArcamFmj( + client, + zone_config.get( + CONF_NAME, + "{} ({}:{}) - {}".format(DEFAULT_NAME, host, port, zone), + ), + zone, + zone_config.get(SERVICE_TURN_ON), + ) + for zone, zone_config in config[CONF_ZONE].items() + ] + ) + + return True + + +class ArcamFmj(MediaPlayerDevice): + """Representation of a media device.""" + + def __init__( + self, client: Client, name: str, zone: int, turn_on: ConfigType + ): + """Initialize device.""" + super().__init__() + self._client = client + self._state = State(client, zone) + self._name = name + self._turn_on = turn_on + self._support = ( + SUPPORT_SELECT_SOURCE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_STEP + | SUPPORT_TURN_OFF + ) + if zone == 1: + self._support |= SUPPORT_SELECT_SOUND_MODE + + def _get_2ch(self): + """Return if source is 2 channel or not""" + audio_format, _ = self._state.get_incoming_audio_format() + return bool( + audio_format + in ( + IncomingAudioFormat.PCM, + IncomingAudioFormat.ANALOGUE_DIRECT, + None, + ) + ) + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return "{}_{}_{}".format( + self._client.host, self._client.port, self._state.zn + ) + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "identifiers": {(DOMAIN, self._client.host, self._client.port)}, + "model": "FMJ", + "manufacturer": "Arcam", + } + + @property + def should_poll(self) -> bool: + """No need to poll.""" + return False + + @property + def name(self): + """Return the name of the controlled device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._state.get_power(): + return STATE_ON + return STATE_OFF + + @property + def supported_features(self): + """Flag media player features that are supported.""" + support = self._support + if self._state.get_power() is not None or self._turn_on: + support |= SUPPORT_TURN_ON + return support + + async def async_added_to_hass(self): + """Once registed add listener for events.""" + + await self._state.start() + + def _data(host): + if host == self._client.host: + self.async_schedule_update_ha_state() + + def _started(host): + if host == self._client.host: + self.async_schedule_update_ha_state(force_refresh=True) + + def _stopped(host): + if host == self._client.host: + self.async_schedule_update_ha_state(force_refresh=True) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_DATA, _data + ) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_STARTED, _started + ) + + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_STOPPED, _stopped + ) + + async def async_update(self): + """Force update state""" + _LOGGER.info("Update state %s", self.name) + await self._state.update() + + async def async_mute_volume(self, mute): + """Send mute command.""" + await self._state.set_mute(mute) + self.async_schedule_update_ha_state() + + async def async_select_source(self, source): + """Select a specific source.""" + value = SourceCodes[source] + await self._state.set_source(value) + self.async_schedule_update_ha_state() + + async def async_select_sound_mode(self, sound_mode): + """Select a specific source.""" + if self._get_2ch(): + await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) + else: + await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) + self.async_schedule_update_ha_state() + + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self._state.set_volume(round(volume * 99.0)) + self.async_schedule_update_ha_state() + + async def async_volume_up(self): + """Turn volume up for media player.""" + await self._state.inc_volume() + self.async_schedule_update_ha_state() + + async def async_volume_down(self): + """Turn volume up for media player.""" + await self._state.dec_volume() + self.async_schedule_update_ha_state() + + async def async_turn_on(self): + """Turn the media player on.""" + if self._state.get_power() is not None: + _LOGGER.info("Turning on device using connection") + await self._state.set_power(True) + elif self._turn_on: + _LOGGER.info("Turning on device using service call") + await async_call_from_config( + self.hass, + self._turn_on, + variables=None, + blocking=True, + validate_config=False, + ) + else: + _LOGGER.error("Unable to turn on") + + async def async_turn_off(self): + """Turn the media player off.""" + await self._state.set_power(False) + + @property + def source(self): + """Return the current input source.""" + value = self._state.get_source() + if value is None: + return None + return value.name + + @property + def source_list(self): + """List of available input sources.""" + return [x.name for x in self._state.get_source_list()] + + @property + def sound_mode(self): + """Name of the current sound mode.""" + if self._state.zn != 1: + return None + + if self._get_2ch(): + value = self._state.get_decode_mode_2ch() + else: + value = self._state.get_decode_mode_mch() + if value: + return value.name + return None + + @property + def sound_mode_list(self): + """List of available sound modes.""" + if self._state.zn != 1: + return None + + if self._get_2ch(): + return [x.name for x in DecodeMode2CH] + return [x.name for x in DecodeModeMCH] + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + value = self._state.get_mute() + if value is None: + return None + return value + + @property + def volume_level(self): + value = self._state.get_volume() + if value: + return value / 99.0 + return None + + @property + def media_content_type(self): + """Content type of current playing media.""" + source = self._state.get_source() + if source == SourceCodes.DAB: + value = MEDIA_TYPE_MUSIC + elif source == SourceCodes.FM: + value = MEDIA_TYPE_MUSIC + else: + value = None + return value + + @property + def media_channel(self): + """Channel currently playing.""" + source = self._state.get_source() + if source == SourceCodes.DAB: + value = self._state.get_dab_station() + elif source == SourceCodes.FM: + value = self._state.get_rds_information() + else: + value = None + return value + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + source = self._state.get_source() + if source == SourceCodes.DAB: + value = self._state.get_dls_pdt() + else: + value = None + return value + + @property + def media_title(self): + """Title of current playing media.""" + source = self._state.get_source() + if source is None: + return None + + channel = self.media_channel + + if channel: + value = "{} - {}".format(source.name, channel) + else: + value = source.name + return value diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json new file mode 100644 index 00000000000000..5844c277364d04 --- /dev/null +++ b/homeassistant/components/arcam_fmj/strings.json @@ -0,0 +1,8 @@ +{ + "config": { + "title": "Arcam FMJ", + "step": {}, + "error": {}, + "abort": {} + } +} From 2bbc589c1ac7265246d037b54bbec36967fe46f4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 10 Jun 2019 22:35:40 +0200 Subject: [PATCH 02/23] Just use use state in player avoid direct client access --- .../components/arcam_fmj/media_player.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 5ba8d527571f58..b9b40901c967e8 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -63,12 +63,11 @@ async def async_setup_entry( async_add_entities( [ ArcamFmj( - client, + State(client, zone), zone_config.get( CONF_NAME, "{} ({}:{}) - {}".format(DEFAULT_NAME, host, port, zone), ), - zone, zone_config.get(SERVICE_TURN_ON), ) for zone, zone_config in config[CONF_ZONE].items() @@ -82,12 +81,11 @@ class ArcamFmj(MediaPlayerDevice): """Representation of a media device.""" def __init__( - self, client: Client, name: str, zone: int, turn_on: ConfigType + self, state: State, name: str, turn_on: Optional[ConfigType] ): """Initialize device.""" super().__init__() - self._client = client - self._state = State(client, zone) + self._state = state self._name = name self._turn_on = turn_on self._support = ( @@ -97,7 +95,7 @@ def __init__( | SUPPORT_VOLUME_STEP | SUPPORT_TURN_OFF ) - if zone == 1: + if state.zn == 1: self._support |= SUPPORT_SELECT_SOUND_MODE def _get_2ch(self): @@ -116,14 +114,14 @@ def _get_2ch(self): def unique_id(self) -> Optional[str]: """Return a unique ID.""" return "{}_{}_{}".format( - self._client.host, self._client.port, self._state.zn + self._state.client.host, self._state.client.port, self._state.zn ) @property def device_info(self): """Return a device description for device registry.""" return { - "identifiers": {(DOMAIN, self._client.host, self._client.port)}, + "identifiers": {(DOMAIN, self._state.client.host, self._state.client.port)}, "model": "FMJ", "manufacturer": "Arcam", } @@ -159,15 +157,15 @@ async def async_added_to_hass(self): await self._state.start() def _data(host): - if host == self._client.host: + if host == self._state.client.host: self.async_schedule_update_ha_state() def _started(host): - if host == self._client.host: + if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) def _stopped(host): - if host == self._client.host: + if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) self.hass.helpers.dispatcher.async_dispatcher_connect( From 44ae043adefc50fb2c76366ce1a7d089d7f227b9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Jun 2019 17:42:06 +0200 Subject: [PATCH 03/23] Avoid leaking exceptions on invalid data --- .../components/arcam_fmj/media_player.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index b9b40901c967e8..4598076104cc4e 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -192,16 +192,26 @@ async def async_mute_volume(self, mute): async def async_select_source(self, source): """Select a specific source.""" - value = SourceCodes[source] + try: + value = SourceCodes[source] + except KeyError: + _LOGGER.error("Unsupported source %s", source) + return + await self._state.set_source(value) self.async_schedule_update_ha_state() async def async_select_sound_mode(self, sound_mode): """Select a specific source.""" - if self._get_2ch(): - await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) - else: - await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) + try: + if self._get_2ch(): + await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) + else: + await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) + except KeyError: + _LOGGER.error("Unsupported sound_mode %s", sound_mode) + return + self.async_schedule_update_ha_state() async def async_set_volume_level(self, volume): From b80b02b5c5cf397ac15e84407a6ebaea5f3d6113 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Jun 2019 21:40:31 +0200 Subject: [PATCH 04/23] Fix return value for volume in case of 0 --- homeassistant/components/arcam_fmj/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 4598076104cc4e..d9059e0a857d8a 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -298,9 +298,9 @@ def is_volume_muted(self): @property def volume_level(self): value = self._state.get_volume() - if value: - return value / 99.0 - return None + if value is None: + return None + return value / 99.0 @property def media_content_type(self): From 6c0bf4cff0d54e93cf6469f2ef2f502f85587fe3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jun 2019 18:58:57 +0200 Subject: [PATCH 05/23] Mark component as having no coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index c8213378e91889..7c6d98bcd65bbe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -38,6 +38,7 @@ omit = homeassistant/components/apple_tv/* homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py + homeassistant/components/arcam_fmj/* homeassistant/components/arduino/* homeassistant/components/arest/binary_sensor.py homeassistant/components/arest/sensor.py From e582e17eca5c4bd734d0ecff65842ee92eaad6f3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jun 2019 19:05:35 +0200 Subject: [PATCH 06/23] Add new requirement --- requirements_all.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index efe78e0b93f9f5..9f71cc32d3b174 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -201,6 +201,9 @@ aprslib==0.6.46 # homeassistant.components.aqualogic aqualogic==1.0 +# homeassistant.components.arcam_fmj +arcam-fmj==0.4.2 + # homeassistant.components.ampio asmog==0.0.6 From 349f7219863084f94e3cf40a614bbf5b0138617a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jun 2019 19:08:05 +0200 Subject: [PATCH 07/23] Add myself as maintainer --- CODEOWNERS | 1 + homeassistant/components/arcam_fmj/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6555e58c88a011..694641fadbcf09 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 7cbd91100910c4..7419bc6cce5c74 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -7,5 +7,7 @@ "arcam-fmj==0.4.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@elupus" + ] } From 68ed46c006391b4a5369601bf61e67b934fe83d8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 19 Jun 2019 20:09:53 +0200 Subject: [PATCH 08/23] Correct linting errors --- .../components/arcam_fmj/__init__.py | 12 +++++---- homeassistant/components/arcam_fmj/const.py | 2 +- .../components/arcam_fmj/media_player.py | 27 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 99b31030a074dc..b98e6f77bddd2a 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -17,8 +17,6 @@ CONF_SCAN_INTERVAL, CONF_ZONE, SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, ) from .const import ( DOMAIN, @@ -91,13 +89,15 @@ 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]} + { + CONF_HOST: entry.data[CONF_HOST], + CONF_PORT: entry.data[CONF_PORT], + } ), ) @@ -106,7 +106,9 @@ async def async_setup_entry( "config": config, } - asyncio.ensure_future(_run_client(hass, client, config[CONF_SCAN_INTERVAL])) + asyncio.ensure_future( + _run_client(hass, client, config[CONF_SCAN_INTERVAL]) + ) hass.async_add_job( hass.config_entries.async_forward_entry_setup(entry, "media_player") diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index 8df6746338a069..ae26ae9ef88820 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -1,4 +1,4 @@ -"""Constants used for arcam""" +"""Constants used for arcam.""" DOMAIN = "arcam_fmj" SIGNAL_CLIENT_STARTED = "arcam.client_started" diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index d9059e0a857d8a..96d4e2fff334d3 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,4 +1,4 @@ -"""Arcam media player""" +"""Arcam media player.""" import logging from typing import Optional @@ -8,7 +8,6 @@ IncomingAudioFormat, SourceCodes, ) -from arcam.fmj.client import Client from arcam.fmj.state import State from homeassistant import config_entries @@ -52,7 +51,7 @@ async def async_setup_entry( config_entry: config_entries.ConfigEntry, async_add_entities, ): - """Setup platform.""" + """Set up the configuration entry.""" data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id] client = data["client"] config = data["config"] @@ -80,9 +79,7 @@ async def async_setup_entry( class ArcamFmj(MediaPlayerDevice): """Representation of a media device.""" - def __init__( - self, state: State, name: str, turn_on: Optional[ConfigType] - ): + def __init__(self, state: State, name: str, turn_on: Optional[ConfigType]): """Initialize device.""" super().__init__() self._state = state @@ -99,7 +96,7 @@ def __init__( self._support |= SUPPORT_SELECT_SOUND_MODE def _get_2ch(self): - """Return if source is 2 channel or not""" + """Return if source is 2 channel or not.""" audio_format, _ = self._state.get_incoming_audio_format() return bool( audio_format @@ -121,7 +118,9 @@ def unique_id(self) -> Optional[str]: def device_info(self): """Return a device description for device registry.""" return { - "identifiers": {(DOMAIN, self._state.client.host, self._state.client.port)}, + "identifiers": { + (DOMAIN, self._state.client.host, self._state.client.port) + }, "model": "FMJ", "manufacturer": "Arcam", } @@ -153,7 +152,6 @@ def supported_features(self): async def async_added_to_hass(self): """Once registed add listener for events.""" - await self._state.start() def _data(host): @@ -181,7 +179,7 @@ def _stopped(host): ) async def async_update(self): - """Force update state""" + """Force update of state.""" _LOGGER.info("Update state %s", self.name) await self._state.update() @@ -205,9 +203,13 @@ async def async_select_sound_mode(self, sound_mode): """Select a specific source.""" try: if self._get_2ch(): - await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) + await self._state.set_decode_mode_2ch( + DecodeMode2CH[sound_mode] + ) else: - await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) + await self._state.set_decode_mode_mch( + DecodeModeMCH[sound_mode] + ) except KeyError: _LOGGER.error("Unsupported sound_mode %s", sound_mode) return @@ -297,6 +299,7 @@ def is_volume_muted(self): @property def volume_level(self): + """Volume level of device.""" value = self._state.get_volume() if value is None: return None From 659d4812ad839c2434936aae4b86d024d03749a6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jun 2019 13:15:02 +0200 Subject: [PATCH 09/23] Use async_create_task instead of async_add_job --- homeassistant/components/arcam_fmj/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index b98e6f77bddd2a..9efdb4bcf3342b 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): (device[CONF_HOST], device[CONF_PORT]) ] = device - hass.async_add_job( + hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -110,7 +110,7 @@ async def async_setup_entry( _run_client(hass, client, config[CONF_SCAN_INTERVAL]) ) - hass.async_add_job( + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "media_player") ) From d8d50495e79e79c13121fb47a0b439de373f2d22 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jun 2019 13:16:35 +0200 Subject: [PATCH 10/23] Use new style string format instead of concat --- homeassistant/components/arcam_fmj/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index ae26ae9ef88820..9dcf4fb7936350 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -9,5 +9,5 @@ DEFAULT_NAME = "Arcam FMJ" DEFAULT_SCAN_INTERVAL = 5 -DOMAIN_DATA_ENTRIES = DOMAIN + ".entries" -DOMAIN_DATA_CONFIG = DOMAIN + ".config" +DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN) +DOMAIN_DATA_CONFIG = "{}.config".format(DOMAIN) From 70e694019d2de0029e0f92e0ea39428082af2cc1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jun 2019 13:17:09 +0200 Subject: [PATCH 11/23] Don't call init of base class without init --- homeassistant/components/arcam_fmj/media_player.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 96d4e2fff334d3..d30604c1d5b060 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -81,7 +81,6 @@ class ArcamFmj(MediaPlayerDevice): def __init__(self, state: State, name: str, turn_on: Optional[ConfigType]): """Initialize device.""" - super().__init__() self._state = state self._name = name self._turn_on = turn_on From 0d16fed3594ca09620307bb77c88b8a5f6c102b6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jun 2019 13:19:47 +0200 Subject: [PATCH 12/23] Annotate callbacks with @callback Otherwise they won't be called in loop --- homeassistant/components/arcam_fmj/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index d30604c1d5b060..71fb903e413a8e 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -11,6 +11,7 @@ from arcam.fmj.state import State from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, @@ -153,14 +154,17 @@ async def async_added_to_hass(self): """Once registed add listener for events.""" await self._state.start() + @callback def _data(host): if host == self._state.client.host: self.async_schedule_update_ha_state() + @callback def _started(host): if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) + @callback def _stopped(host): if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) From 3ecd2e56a74815f7657969c69673ccf26c0a1e40 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jun 2019 13:21:10 +0200 Subject: [PATCH 13/23] Reduce log level to debug --- homeassistant/components/arcam_fmj/__init__.py | 4 ++-- homeassistant/components/arcam_fmj/media_player.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 9efdb4bcf3342b..a2ecec55c4fb16 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -138,7 +138,7 @@ def _listen(_): try: await asyncio.wait_for(client.start(), timeout=interval) - _LOGGER.info("Client connected %s", client.host) + _LOGGER.debug("Client connected %s", client.host) hass.helpers.dispatcher.async_dispatcher_send( SIGNAL_CLIENT_STARTED, client.host ) @@ -149,7 +149,7 @@ def _listen(_): finally: await client.stop() - _LOGGER.info("Client disconnected %s", client.host) + _LOGGER.debug("Client disconnected %s", client.host) hass.helpers.dispatcher.async_dispatcher_send( SIGNAL_CLIENT_STOPPED, client.host ) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 71fb903e413a8e..240e02ccf52824 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -183,7 +183,7 @@ def _stopped(host): async def async_update(self): """Force update of state.""" - _LOGGER.info("Update state %s", self.name) + _LOGGER.debug("Update state %s", self.name) await self._state.update() async def async_mute_volume(self, mute): @@ -237,10 +237,10 @@ async def async_volume_down(self): async def async_turn_on(self): """Turn the media player on.""" if self._state.get_power() is not None: - _LOGGER.info("Turning on device using connection") + _LOGGER.debug("Turning on device using connection") await self._state.set_power(True) elif self._turn_on: - _LOGGER.info("Turning on device using service call") + _LOGGER.debug("Turning on device using service call") await async_call_from_config( self.hass, self._turn_on, From efbb02418894074bb6e29339bc601b808faa991e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 20 Jun 2019 13:34:53 +0200 Subject: [PATCH 14/23] Use async_timeout instead of wait_for --- homeassistant/components/arcam_fmj/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index a2ecec55c4fb16..5ec384b6339ffc 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -3,6 +3,7 @@ import asyncio import voluptuous as vol +import async_timeout from arcam.fmj.client import Client from arcam.fmj import ConnectionFailed @@ -136,7 +137,8 @@ def _listen(_): while run: try: - await asyncio.wait_for(client.start(), timeout=interval) + with async_timeout.timeout(interval): + await client.start() _LOGGER.debug("Client connected %s", client.host) hass.helpers.dispatcher.async_dispatcher_send( From 1ee6d1c861772feafdac9b3ee31fc1db05419898 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 2 Jul 2019 00:44:25 +0200 Subject: [PATCH 15/23] Bump to version of arcam_fmj supporting 3.5 --- homeassistant/components/arcam_fmj/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 7419bc6cce5c74..59ab3c03d9273e 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -4,7 +4,7 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/components/arcam_fmj", "requirements": [ - "arcam-fmj==0.4.2" + "arcam-fmj==0.4.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9f71cc32d3b174..75781e83c8675d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -202,7 +202,7 @@ aprslib==0.6.46 aqualogic==1.0 # homeassistant.components.arcam_fmj -arcam-fmj==0.4.2 +arcam-fmj==0.4.3 # homeassistant.components.ampio asmog==0.0.6 From 68de03ec99e5f17ac370f881d8917eb3c96c8281 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 2 Jul 2019 01:30:22 +0200 Subject: [PATCH 16/23] Fix extra spaces --- homeassistant/components/arcam_fmj/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index 9dcf4fb7936350..b065e1a0833290 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -9,5 +9,5 @@ DEFAULT_NAME = "Arcam FMJ" DEFAULT_SCAN_INTERVAL = 5 -DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN) +DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN) DOMAIN_DATA_CONFIG = "{}.config".format(DOMAIN) From e0d3745823b7c242b99a09afa48f4c2a88439901 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 2 Jul 2019 01:31:08 +0200 Subject: [PATCH 17/23] Drop somewhat flaky unique_id --- homeassistant/components/arcam_fmj/media_player.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 240e02ccf52824..c4142250d870e9 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -107,13 +107,6 @@ def _get_2ch(self): ) ) - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return "{}_{}_{}".format( - self._state.client.host, self._state.client.port, self._state.zn - ) - @property def device_info(self): """Return a device description for device registry.""" From a7a0d6823ed74a4a311be384ad1f91e154f949f7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 2 Jul 2019 02:28:10 +0200 Subject: [PATCH 18/23] Un-blackify ident to satisy pylint --- homeassistant/components/arcam_fmj/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index c4142250d870e9..d8148e86e1eaca 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -48,9 +48,9 @@ async def async_setup_entry( - hass: HomeAssistantType, - config_entry: config_entries.ConfigEntry, - async_add_entities, + hass: HomeAssistantType, + config_entry: config_entries.ConfigEntry, + async_add_entities, ): """Set up the configuration entry.""" data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id] From a996589e8b2127c9235d1a3e735d7d46ed01e9bf Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 2 Jul 2019 13:25:57 +0200 Subject: [PATCH 19/23] Un-blackify ident to satisy pylint --- homeassistant/components/arcam_fmj/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 5ec384b6339ffc..66b6314783c6f7 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -87,7 +87,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry( - hass: HomeAssistantType, entry: config_entries.ConfigEntry + 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]) From 8b2ac93ca6f661e6b1c82d85b0bcd13484a02dab Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 7 Jul 2019 18:10:00 +0200 Subject: [PATCH 20/23] Move default name calculation to config validation --- .../components/arcam_fmj/__init__.py | 20 ++++++++++++++++--- .../components/arcam_fmj/media_player.py | 11 +--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 66b6314783c6f7..0fffa2bbb5c8dc 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -23,6 +23,7 @@ DOMAIN, DOMAIN_DATA_ENTRIES, DOMAIN_DATA_CONFIG, + DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, SIGNAL_CLIENT_DATA, @@ -39,6 +40,17 @@ def _optional_zone(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, @@ -47,14 +59,16 @@ def _optional_zone(value): ) 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): {vol.In([1, 2]): _optional_zone}, + 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( diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index d8148e86e1eaca..b22f40a641d4d1 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -26,8 +26,6 @@ from homeassistant.const import ( CONF_NAME, CONF_ZONE, - CONF_HOST, - CONF_PORT, SERVICE_TURN_ON, STATE_OFF, STATE_ON, @@ -39,7 +37,6 @@ SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, - DEFAULT_NAME, DOMAIN_DATA_ENTRIES, DOMAIN, ) @@ -57,17 +54,11 @@ async def async_setup_entry( client = data["client"] config = data["config"] - host = config[CONF_HOST] - port = config[CONF_PORT] - async_add_entities( [ ArcamFmj( State(client, zone), - zone_config.get( - CONF_NAME, - "{} ({}:{}) - {}".format(DEFAULT_NAME, host, port, zone), - ), + zone_config[CONF_NAME], zone_config.get(SERVICE_TURN_ON), ) for zone, zone_config in config[CONF_ZONE].items() From 51834eaa65ed3270d40f8089b5fbb46180151a1f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 10 Jun 2019 23:21:57 +0200 Subject: [PATCH 21/23] Add test folder --- tests/components/arcam_fmj/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/components/arcam_fmj/__init__.py diff --git a/tests/components/arcam_fmj/__init__.py b/tests/components/arcam_fmj/__init__.py new file mode 100644 index 00000000000000..bc4814be06c84b --- /dev/null +++ b/tests/components/arcam_fmj/__init__.py @@ -0,0 +1 @@ +"""Tests for the arcam_fmj component.""" From 8aa17430dfce9bb1475b4b4d7b2991e52cc9d6b8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 8 Jul 2019 12:51:51 +0200 Subject: [PATCH 22/23] Drop unused code --- homeassistant/components/arcam_fmj/config_flow.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py index 7cabbc7e318bb7..a92a2ec52a6211 100644 --- a/homeassistant/components/arcam_fmj/config_flow.py +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -1,14 +1,11 @@ -"""Config flow to configure the SimpliSafe component.""" -from collections import OrderedDict +"""Config flow to configure the Arcam FMJ component.""" 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) @@ -19,10 +16,6 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow): 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): """Import a config entry from configuration.yaml.""" entries = self.hass.config_entries.async_entries(DOMAIN) From 10fd948eecdb32b28a22db217ae5b322c7016a34 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 8 Jul 2019 13:17:48 +0200 Subject: [PATCH 23/23] Add tests for config flow import --- .coveragerc | 3 +- .../components/arcam_fmj/test_config_flow.py | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/components/arcam_fmj/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7c6d98bcd65bbe..81afd86ac5769b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -38,7 +38,8 @@ omit = homeassistant/components/apple_tv/* homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py - homeassistant/components/arcam_fmj/* + 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 diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py new file mode 100644 index 00000000000000..60b34016cd99a8 --- /dev/null +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -0,0 +1,50 @@ + +"""Tests for the Arcam FMJ config flow module.""" +import pytest +from homeassistant import data_entry_flow +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry, MockDependency + +with MockDependency('arcam'), \ + MockDependency('arcam.fmj'), \ + MockDependency('arcam.fmj.client'): + from homeassistant.components.arcam_fmj import DEVICE_SCHEMA + from homeassistant.components.arcam_fmj.config_flow import ( + ArcamFmjFlowHandler) + from homeassistant.components.arcam_fmj.const import DOMAIN + + MOCK_HOST = "127.0.0.1" + MOCK_PORT = 1234 + MOCK_NAME = "Arcam FMJ" + MOCK_CONFIG = DEVICE_SCHEMA({ + CONF_HOST: MOCK_HOST, + CONF_PORT: MOCK_PORT, + }) + + @pytest.fixture(name="config_entry") + def config_entry_fixture(): + """Create a mock HEOS config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + title=MOCK_NAME, + ) + + async def test_single_import_only(hass, config_entry): + """Test form is shown when host not provided.""" + config_entry.add_to_hass(hass) + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'already_setup' + + async def test_import(hass): + """Test form is shown when host not provided.""" + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == MOCK_NAME + assert result['data'] == MOCK_CONFIG