diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index f08d4dcd151..540e39f8f44 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ATTR_ACTIVITY_NOTIFY, DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS +from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS from .remote import HarmonyRemote _LOGGER = logging.getLogger(__name__) @@ -38,18 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name = entry.data[CONF_NAME] activity = entry.options.get(ATTR_ACTIVITY) delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) - activity_notify = entry.options.get(ATTR_ACTIVITY_NOTIFY, False) harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") try: device = HarmonyRemote( - name, - entry.unique_id, - address, - activity, - harmony_conf_file, - delay_secs, - activity_notify, + name, entry.unique_id, address, activity, harmony_conf_file, delay_secs ) connected_ok = await device.connect() except (asyncio.TimeoutError, ValueError, AttributeError): diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 8487509407c..576451ef2d6 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.remote import ( from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from .const import ATTR_ACTIVITY_NOTIFY, DOMAIN, UNIQUE_ID +from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID from .util import ( find_best_name_for_remote, find_unique_id_for_remote, @@ -148,7 +148,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_valid_input(self, validated, user_input): """Single path to create the config entry from validated input.""" - data = {CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]} + data = { + CONF_NAME: validated[CONF_NAME], + CONF_HOST: validated[CONF_HOST], + } # Options from yaml are preserved, we will pull them out when # we setup the config entry data.update(_options_from_user_input(user_input)) @@ -162,8 +165,6 @@ def _options_from_user_input(user_input): options[ATTR_ACTIVITY] = user_input[ATTR_ACTIVITY] if ATTR_DELAY_SECS in user_input: options[ATTR_DELAY_SECS] = user_input[ATTR_DELAY_SECS] - if ATTR_ACTIVITY_NOTIFY in user_input: - options[ATTR_ACTIVITY_NOTIFY] = user_input[ATTR_ACTIVITY_NOTIFY] return options @@ -190,12 +191,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ): vol.Coerce(float), vol.Optional( - ATTR_ACTIVITY, default=self.config_entry.options.get(ATTR_ACTIVITY), - ): vol.In(remote.activity_names), - vol.Optional( - ATTR_ACTIVITY_NOTIFY, - default=self.config_entry.options.get(ATTR_ACTIVITY_NOTIFY, False), - ): vol.Coerce(bool), + ATTR_ACTIVITY, + default=self.config_entry.options.get( + ATTR_ACTIVITY, PREVIOUS_ACTIVE_ACTIVITY + ), + ): vol.In([PREVIOUS_ACTIVE_ACTIVITY, *remote.activity_names]), } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index dcb4f74912f..26368810c83 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -6,4 +6,8 @@ PLATFORMS = ["remote"] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" -ATTR_ACTIVITY_NOTIFY = "activity_notify" +ATTR_ACTIVITY_LIST = "activity_list" +ATTR_DEVICES_LIST = "devices_list" +ATTR_LAST_ACTIVITY = "last_activity" +ATTR_CURRENT_ACTIVITY = "current_activity" +PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 962c11a8cb3..7b49321c7b0 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -25,12 +25,17 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ACTIVITY_POWER_OFF, - ATTR_ACTIVITY_NOTIFY, + ATTR_ACTIVITY_LIST, + ATTR_CURRENT_ACTIVITY, + ATTR_DEVICES_LIST, + ATTR_LAST_ACTIVITY, DOMAIN, HARMONY_OPTIONS_UPDATE, + PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, UNIQUE_ID, @@ -40,6 +45,7 @@ from .util import ( find_matching_config_entries_for_host, find_unique_id_for_remote, get_harmony_client_if_available, + list_names_from_hublist, ) _LOGGER = logging.getLogger(__name__) @@ -48,7 +54,6 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -ATTR_CURRENT_ACTIVITY = "current_activity" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -123,12 +128,10 @@ async def async_setup_entry( ) -class HarmonyRemote(remote.RemoteEntity): +class HarmonyRemote(remote.RemoteEntity, RestoreEntity): """Remote representation used to control a Harmony device.""" - def __init__( - self, name, unique_id, host, activity, out_path, delay_secs, activity_notify - ): + def __init__(self, name, unique_id, host, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" self._name = name self.host = host @@ -140,7 +143,7 @@ class HarmonyRemote(remote.RemoteEntity): self.delay_secs = delay_secs self._available = False self._unique_id = unique_id - self._activity_notify = activity_notify + self._last_activity = None @property def activity_names(self): @@ -163,26 +166,20 @@ class HarmonyRemote(remote.RemoteEntity): if ATTR_ACTIVITY in data: self.default_activity = data[ATTR_ACTIVITY] - if ATTR_ACTIVITY_NOTIFY in data: - self._activity_notify = data[ATTR_ACTIVITY_NOTIFY] - self._update_callbacks() - def _update_callbacks(self): callbacks = { "config_updated": self.new_config, "connect": self.got_connected, "disconnect": self.got_disconnected, - "new_activity_starting": None, + "new_activity_starting": self.new_activity, "new_activity": None, } - if self._activity_notify: - callbacks["new_activity_starting"] = self.new_activity - else: - callbacks["new_activity"] = self.new_activity self._client.callbacks = ClientCallbackType(**callbacks) async def async_added_to_hass(self): """Complete the initialization.""" + await super().async_added_to_hass() + _LOGGER.debug("%s: Harmony Hub added", self._name) # Register the callbacks self._update_callbacks() @@ -199,6 +196,19 @@ class HarmonyRemote(remote.RemoteEntity): # activity await self.new_config() + # Restore the last activity so we know + # how what to turn on if nothing + # is specified + last_state = await self.async_get_last_state() + if not last_state: + return + if ATTR_LAST_ACTIVITY not in last_state.attributes: + return + if self.is_on: + return + + self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY] + async def shutdown(self): """Close connection on shutdown.""" _LOGGER.debug("%s: Closing Harmony Hub", self._name) @@ -241,7 +251,14 @@ class HarmonyRemote(remote.RemoteEntity): @property def device_state_attributes(self): """Add platform specific attributes.""" - return {ATTR_CURRENT_ACTIVITY: self._current_activity} + return { + ATTR_CURRENT_ACTIVITY: self._current_activity, + ATTR_ACTIVITY_LIST: list_names_from_hublist( + self._client.hub_config.activities + ), + ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices), + ATTR_LAST_ACTIVITY: self._last_activity, + } @property def is_on(self): @@ -271,6 +288,11 @@ class HarmonyRemote(remote.RemoteEntity): activity_id, activity_name = activity_info _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name) self._current_activity = activity_name + if activity_id != -1: + # Save the activity so we can restore + # to that activity if none is specified + # when turning on + self._last_activity = activity_name self._state = bool(activity_id != -1) self._available = True self.async_write_ha_state() @@ -306,6 +328,16 @@ class HarmonyRemote(remote.RemoteEntity): activity = kwargs.get(ATTR_ACTIVITY, self.default_activity) + if not activity or activity == PREVIOUS_ACTIVE_ACTIVITY: + if self._last_activity: + activity = self._last_activity + else: + all_activities = list_names_from_hublist( + self._client.hub_config.activities + ) + if all_activities: + activity = all_activities[0] + if activity: activity_id = None if activity.isdigit() or activity == "-1": diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index 053d5cea8bd..86de34672be 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -28,8 +28,7 @@ "description": "Adjust Harmony Hub Options", "data": { "activity": "The default activity to execute when none is specified.", - "delay_secs": "The delay between sending commands.", - "activity_notify": "Update current activity on start of activity switch." + "delay_secs": "The delay between sending commands." } } } diff --git a/homeassistant/components/harmony/translations/en.json b/homeassistant/components/harmony/translations/en.json index d180ff4ba7d..ce13e79e279 100644 --- a/homeassistant/components/harmony/translations/en.json +++ b/homeassistant/components/harmony/translations/en.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "The default activity to execute when none is specified.", - "activity_notify": "Update current activity on start of activity switch.", "delay_secs": "The delay between sending commands." }, "description": "Adjust Harmony Hub Options" diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index daee1845c2d..b0a16004065 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -49,3 +49,14 @@ def find_matching_config_entries_for_host(hass, host): if entry.data[CONF_HOST] == host: return entry return None + + +def list_names_from_hublist(hub_list): + """Extract the name key value from a hub list of names.""" + if not hub_list: + return [] + return [ + element["name"] + for element in hub_list + if element.get("name") and element.get("id") != -1 + ] diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 812e3414ea9..0228983ef9d 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,9 +1,11 @@ """Test the Logitech Harmony Hub config flow.""" -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.harmony.config_flow import CannotConnect -from homeassistant.components.harmony.const import DOMAIN +from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY +from homeassistant.const import CONF_HOST, CONF_NAME -from tests.async_mock import AsyncMock, MagicMock, patch +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry def _get_mock_harmonyapi(connect=None, close=None): @@ -14,6 +16,23 @@ def _get_mock_harmonyapi(connect=None, close=None): return harmonyapi_mock +def _get_mock_harmonyclient(): + harmonyclient_mock = MagicMock() + type(harmonyclient_mock).connect = AsyncMock() + type(harmonyclient_mock).close = AsyncMock() + type(harmonyclient_mock).get_activity_name = MagicMock(return_value="Watch TV") + type(harmonyclient_mock.hub_config).activities = PropertyMock( + return_value=[{"name": "Watch TV", "id": 123}] + ) + type(harmonyclient_mock.hub_config).devices = PropertyMock( + return_value=[{"name": "My TV", "id": 1234}] + ) + type(harmonyclient_mock.hub_config).info = PropertyMock(return_value={}) + type(harmonyclient_mock.hub_config).hub_state = PropertyMock(return_value={}) + + return harmonyclient_mock + + async def test_user_form(hass): """Test we get the user form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -37,10 +56,7 @@ async def test_user_form(hass): assert result2["type"] == "create_entry" assert result2["title"] == "friend" - assert result2["data"] == { - "host": "1.2.3.4", - "name": "friend", - } + assert result2["data"] == {"host": "1.2.3.4", "name": "friend"} await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -66,7 +82,6 @@ async def test_form_import(hass): "name": "friend", "activity": "Watch TV", "delay_secs": 0.9, - "activity_notify": True, "unique_id": "555234534543", }, ) @@ -79,7 +94,6 @@ async def test_form_import(hass): "name": "friend", "activity": "Watch TV", "delay_secs": 0.9, - "activity_notify": True, } # It is not possible to import options at this time # so they end up in the config entry data and are @@ -125,10 +139,7 @@ async def test_form_ssdp(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Harmony Hub" - assert result2["data"] == { - "host": "192.168.1.12", - "name": "Harmony Hub", - } + assert result2["data"] == {"host": "192.168.1.12", "name": "Harmony Hub"} await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -150,9 +161,46 @@ async def test_form_cannot_connect(hass): "name": "friend", "activity": "Watch TV", "delay_secs": 0.2, - "activity_notify": True, }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_options_flow(hass): + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="abcde12345", + data={CONF_HOST: "1.2.3.4", CONF_NAME: "Guest Room"}, + options={"activity": "Watch TV", "delay_secs": 0.5}, + ) + + harmony_client = _get_mock_harmonyclient() + + with patch( + "aioharmony.harmonyapi.HarmonyClient", return_value=harmony_client, + ), patch("homeassistant.components.harmony.remote.HarmonyRemote.write_config_file"): + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "activity": PREVIOUS_ACTIVE_ACTIVITY, + "delay_secs": 0.4, + }