Make devices and activities visible as harmony attributes (#37559)
* Make devices and activities visibile as harmony attributes * Allow restoring previous activity, add tests * fix test * Kill activity_notify with fire * remove trailing ,
This commit is contained in:
parent
ae7d464878
commit
76be95d7e0
8 changed files with 140 additions and 54 deletions
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue