Migrate Harmony to has entity name (#104737)

* Migrate Harmony to has entity name

* Fix tests

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/113812/head
Joost Lekkerkerker 2024-03-19 14:02:50 +01:00 committed by GitHub
parent 6106a66ed8
commit 44211dc761
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 120 additions and 114 deletions

View File

@ -29,7 +29,7 @@ class HarmonyData(HarmonySubscriberMixin):
) -> None:
"""Initialize a data object."""
super().__init__(hass)
self._name = name
self.name = name
self._unique_id = unique_id
self._available = False
self._address = address
@ -60,11 +60,6 @@ class HarmonyData(HarmonySubscriberMixin):
return devices
@property
def name(self) -> str:
"""Return the Harmony device's name."""
return self._name
@property
def unique_id(self):
"""Return the Harmony device's unique_id."""
@ -105,7 +100,7 @@ class HarmonyData(HarmonySubscriberMixin):
async def connect(self) -> None:
"""Connect to the Harmony Hub."""
_LOGGER.debug("%s: Connecting", self._name)
_LOGGER.debug("%s: Connecting", self.name)
callbacks = {
"config_updated": self._config_updated,
@ -124,27 +119,27 @@ class HarmonyData(HarmonySubscriberMixin):
except (TimeoutError, aioexc.TimeOut) as err:
await self._client.close()
raise ConfigEntryNotReady(
f"{self._name}: Connection timed-out to {self._address}:8088"
f"{self.name}: Connection timed-out to {self._address}:8088"
) from err
except (ValueError, AttributeError) as err:
await self._client.close()
raise ConfigEntryNotReady(
f"{self._name}: Error {err} while connected HUB at:"
f"{self.name}: Error {err} while connected HUB at:"
f" {self._address}:8088"
) from err
if not connected:
await self._client.close()
raise ConfigEntryNotReady(
f"{self._name}: Unable to connect to HUB at: {self._address}:8088"
f"{self.name}: Unable to connect to HUB at: {self._address}:8088"
)
async def shutdown(self) -> None:
"""Close connection on shutdown."""
_LOGGER.debug("%s: Closing Harmony Hub", self._name)
_LOGGER.debug("%s: Closing Harmony Hub", self.name)
try:
await self._client.close()
except aioexc.TimeOut:
_LOGGER.warning("%s: Disconnect timed-out", self._name)
_LOGGER.warning("%s: Disconnect timed-out", self.name)
async def async_start_activity(self, activity: str) -> None:
"""Start an activity from the Harmony device."""

View File

@ -19,11 +19,12 @@ TIME_MARK_DISCONNECTED = 10
class HarmonyEntity(Entity):
"""Base entity for Harmony with connection state handling."""
_attr_has_entity_name = True
def __init__(self, data: HarmonyData) -> None:
"""Initialize the Harmony base entity."""
super().__init__()
self._unsub_mark_disconnected: Callable[[], None] | None = None
self._name = data.name
self._data = data
self._attr_should_poll = False
@ -34,14 +35,14 @@ class HarmonyEntity(Entity):
async def async_got_connected(self, _: str | None = None) -> None:
"""Notification that we're connected to the HUB."""
_LOGGER.debug("%s: connected to the HUB", self._name)
_LOGGER.debug("%s: connected to the HUB", self._data.name)
self.async_write_ha_state()
self._clear_disconnection_delay()
async def async_got_disconnected(self, _: str | None = None) -> None:
"""Notification that we're disconnected from the HUB."""
_LOGGER.debug("%s: disconnected from the HUB", self._name)
_LOGGER.debug("%s: disconnected from the HUB", self._data.name)
# We're going to wait for 10 seconds before announcing we're
# unavailable, this to allow a reconnection to happen.
self._unsub_mark_disconnected = async_call_later(

View File

@ -87,6 +87,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
"""Remote representation used to control a Harmony device."""
_attr_supported_features = RemoteEntityFeature.ACTIVITY
_attr_name = None
def __init__(
self, data: HarmonyData, activity: str | None, delay_secs: float, out_path: str
@ -103,7 +104,6 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
self._config_path = out_path
self._attr_unique_id = data.unique_id
self._attr_device_info = self._data.device_info(DOMAIN)
self._attr_name = data.name
async def _async_update_options(self, data: dict[str, Any]) -> None:
"""Change options when the options flow does."""
@ -136,7 +136,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
"""Complete the initialization."""
await super().async_added_to_hass()
_LOGGER.debug("%s: Harmony Hub added", self.name)
_LOGGER.debug("%s: Harmony Hub added", self._data.name)
self.async_on_remove(self._clear_disconnection_delay)
self._setup_callbacks()
@ -193,7 +193,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
def async_new_activity(self, activity_info: tuple) -> None:
"""Call for updating the current activity."""
activity_id, activity_name = activity_info
_LOGGER.debug("%s: activity reported as: %s", self.name, activity_name)
_LOGGER.debug("%s: activity reported as: %s", self._data.name, activity_name)
self._current_activity = activity_name
if self._is_initial_update:
self._is_initial_update = False
@ -209,13 +209,13 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
async def async_new_config(self, _: dict | None = None) -> None:
"""Call for updating the current activity."""
_LOGGER.debug("%s: configuration has been updated", self.name)
_LOGGER.debug("%s: configuration has been updated", self._data.name)
self.async_new_activity(self._data.current_activity)
await self.hass.async_add_executor_job(self.write_config_file)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start an activity from the Harmony device."""
_LOGGER.debug("%s: Turn On", self.name)
_LOGGER.debug("%s: Turn On", self._data.name)
activity = kwargs.get(ATTR_ACTIVITY, self.default_activity)
@ -228,7 +228,9 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
if activity:
await self._data.async_start_activity(activity)
else:
_LOGGER.error("%s: No activity specified with turn_on service", self.name)
_LOGGER.error(
"%s: No activity specified with turn_on service", self._data.name
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Start the PowerOff activity."""
@ -236,9 +238,9 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a list of commands to one device."""
_LOGGER.debug("%s: Send Command", self.name)
_LOGGER.debug("%s: Send Command", self._data.name)
if (device := kwargs.get(ATTR_DEVICE)) is None:
_LOGGER.error("%s: Missing required argument: device", self.name)
_LOGGER.error("%s: Missing required argument: device", self._data.name)
return
num_repeats = kwargs[ATTR_NUM_REPEATS]
@ -263,10 +265,12 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
This is a handy way for users to figure out the available commands for automations.
"""
_LOGGER.debug(
"%s: Writing hub configuration to file: %s", self.name, self._config_path
"%s: Writing hub configuration to file: %s",
self._data.name,
self._config_path,
)
if (json_config := self._data.json_config) is None:
_LOGGER.warning("%s: No configuration received from hub", self.name)
_LOGGER.warning("%s: No configuration received from hub", self._data.name)
return
try:
@ -275,7 +279,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity):
except OSError as exc:
_LOGGER.error(
"%s: Unable to write HUB configuration to %s: %s",
self.name,
self._data.name,
self._config_path,
exc,
)

View File

@ -6,7 +6,6 @@ import logging
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -25,10 +24,7 @@ async def async_setup_entry(
) -> None:
"""Set up harmony activities select."""
data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA]
_LOGGER.debug("creating select for %s hub activities", entry.data[CONF_NAME])
async_add_entities(
[HarmonyActivitySelect(f"{entry.data[CONF_NAME]} Activities", data)]
)
async_add_entities([HarmonyActivitySelect(data)])
class HarmonyActivitySelect(HarmonyEntity, SelectEntity):
@ -36,13 +32,12 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity):
_attr_translation_key = "activities"
def __init__(self, name: str, data: HarmonyData) -> None:
def __init__(self, data: HarmonyData) -> None:
"""Initialize HarmonyActivitySelect class."""
super().__init__(data=data)
self._data = data
self._attr_unique_id = self._data.unique_id
self._attr_device_info = self._data.device_info(DOMAIN)
self._attr_name = name
@property
def options(self) -> list[str]:

View File

@ -39,6 +39,7 @@
"entity": {
"select": {
"activities": {
"name": "Activities",
"state": {
"power_off": "Power Off"
}

View File

@ -7,7 +7,6 @@ from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HassJob, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
@ -25,28 +24,22 @@ async def async_setup_entry(
) -> None:
"""Set up harmony activity switches."""
data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA]
activities = data.activities
switches = []
for activity in activities:
_LOGGER.debug("creating switch for activity: %s", activity)
name = f"{entry.data[CONF_NAME]} {activity['label']}"
switches.append(HarmonyActivitySwitch(name, activity, data))
async_add_entities(switches, True)
async_add_entities(
(HarmonyActivitySwitch(activity, data) for activity in data.activities), True
)
class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity):
"""Switch representation of a Harmony activity."""
def __init__(self, name: str, activity: dict, data: HarmonyData) -> None:
def __init__(self, activity: dict, data: HarmonyData) -> None:
"""Initialize HarmonyActivitySwitch class."""
super().__init__(data=data)
self._activity_name = activity["label"]
self._activity_name = self._attr_name = activity["label"]
self._activity_id = activity["id"]
self._attr_entity_registry_enabled_default = False
self._attr_unique_id = f"activity_{self._activity_id}"
self._attr_name = name
self._attr_device_info = self._data.device_info(DOMAIN)
@property

View File

@ -5,9 +5,17 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from aioharmony.const import ClientCallbackType
import pytest
from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF
from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF, DOMAIN
from homeassistant.const import CONF_HOST, CONF_NAME
from .const import NILE_TV_ACTIVITY_ID, PLAY_MUSIC_ACTIVITY_ID, WATCH_TV_ACTIVITY_ID
from .const import (
HUB_NAME,
NILE_TV_ACTIVITY_ID,
PLAY_MUSIC_ACTIVITY_ID,
WATCH_TV_ACTIVITY_ID,
)
from tests.common import MockConfigEntry
ACTIVITIES_TO_IDS = {
ACTIVITY_POWER_OFF: -1,
@ -166,3 +174,13 @@ def mock_write_config():
"homeassistant.components.harmony.remote.HarmonyRemote.write_config_file",
) as mock:
yield mock
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
unique_id="123",
data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME},
)

View File

@ -43,15 +43,16 @@ STOP_COMMAND = "Stop"
async def test_connection_state_changes(
harmony_client, mock_hc, hass: HomeAssistant, mock_write_config
harmony_client,
mock_hc,
hass: HomeAssistant,
mock_write_config,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure connection changes are reflected in the remote state."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# mocks start with current activity == Watch TV
@ -82,14 +83,13 @@ async def test_connection_state_changes(
assert hass.states.is_state(ENTITY_REMOTE, STATE_ON)
async def test_remote_toggles(mock_hc, hass: HomeAssistant, mock_write_config) -> None:
async def test_remote_toggles(
mock_hc, hass: HomeAssistant, mock_write_config, mock_config_entry: MockConfigEntry
) -> None:
"""Ensure calls to the remote also updates the switches."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# mocks start remote with Watch TV default activity
@ -151,15 +151,16 @@ async def test_remote_toggles(mock_hc, hass: HomeAssistant, mock_write_config) -
async def test_async_send_command(
mock_hc, harmony_client, hass: HomeAssistant, mock_write_config
mock_hc,
harmony_client,
hass: HomeAssistant,
mock_write_config,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure calls to send remote commands properly propagate to devices."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
send_commands_mock = harmony_client.send_commands
@ -285,11 +286,16 @@ async def test_async_send_command(
async def test_async_send_command_custom_delay(
mock_hc, harmony_client, hass: HomeAssistant, mock_write_config
mock_hc,
harmony_client,
hass: HomeAssistant,
mock_write_config,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure calls to send remote commands properly propagate to devices with custom delays."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="123",
data={
CONF_HOST: "192.0.2.0",
CONF_NAME: HUB_NAME,
@ -327,15 +333,16 @@ async def test_async_send_command_custom_delay(
async def test_change_channel(
mock_hc, harmony_client, hass: HomeAssistant, mock_write_config
mock_hc,
harmony_client,
hass: HomeAssistant,
mock_write_config,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test change channel commands."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
change_channel_mock = harmony_client.change_channel
@ -353,15 +360,16 @@ async def test_change_channel(
async def test_sync(
mock_hc, harmony_client, mock_write_config, hass: HomeAssistant
mock_hc,
harmony_client,
mock_write_config,
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the sync command."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
sync_mock = harmony_client.sync

View File

@ -2,38 +2,31 @@
from datetime import timedelta
from homeassistant.components.harmony.const import DOMAIN
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_NAME,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.util import utcnow
from .const import ENTITY_REMOTE, ENTITY_SELECT, HUB_NAME
from .const import ENTITY_REMOTE, ENTITY_SELECT
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_connection_state_changes(
harmony_client, mock_hc, hass: HomeAssistant, mock_write_config
harmony_client,
mock_hc,
hass: HomeAssistant,
mock_write_config,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure connection changes are reflected in the switch states."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# mocks start with current activity == Watch TV
@ -56,14 +49,13 @@ async def test_connection_state_changes(
assert hass.states.is_state(ENTITY_SELECT, "Watch TV")
async def test_options(mock_hc, hass: HomeAssistant, mock_write_config) -> None:
async def test_options(
mock_hc, hass: HomeAssistant, mock_write_config, mock_config_entry: MockConfigEntry
) -> None:
"""Ensure calls to the switch modify the harmony state."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# assert we have all options
@ -76,14 +68,12 @@ async def test_options(mock_hc, hass: HomeAssistant, mock_write_config) -> None:
]
async def test_select_option(mock_hc, hass: HomeAssistant, mock_write_config) -> None:
async def test_select_option(
mock_hc, hass: HomeAssistant, mock_write_config, mock_config_entry: MockConfigEntry
) -> None:
"""Ensure calls to the switch modify the harmony state."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# mocks start with current activity == Watch TV

View File

@ -90,21 +90,22 @@ async def test_connection_state_changes(
async def test_switch_toggles(
mock_hc, hass: HomeAssistant, mock_write_config, entity_registry: er.EntityRegistry
mock_hc,
hass: HomeAssistant,
mock_write_config,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Ensure calls to the switch modify the harmony state."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# enable switch entities
entity_registry.async_update_entity(ENTITY_WATCH_TV, disabled_by=None)
entity_registry.async_update_entity(ENTITY_PLAY_MUSIC, disabled_by=None)
await hass.config_entries.async_reload(entry.entry_id)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
# mocks start with current activity == Watch TV