Home Assistant Unofficial Reference 2024.12.1
remote.py
Go to the documentation of this file.
1 """Support for Harmony Hub devices."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 import json
7 import logging
8 from typing import Any
9 
10 import voluptuous as vol
11 
13  ATTR_ACTIVITY,
14  ATTR_DELAY_SECS,
15  ATTR_DEVICE,
16  ATTR_HOLD_SECS,
17  ATTR_NUM_REPEATS,
18  DEFAULT_DELAY_SECS,
19  RemoteEntity,
20  RemoteEntityFeature,
21 )
22 from homeassistant.core import HassJob, HomeAssistant, callback
23 from homeassistant.helpers import entity_platform
25 from homeassistant.helpers.dispatcher import async_dispatcher_connect
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.restore_state import RestoreEntity
28 from homeassistant.helpers.typing import VolDictType
29 
30 from .const import (
31  ACTIVITY_POWER_OFF,
32  ATTR_ACTIVITY_STARTING,
33  ATTR_DEVICES_LIST,
34  ATTR_LAST_ACTIVITY,
35  DOMAIN,
36  HARMONY_OPTIONS_UPDATE,
37  PREVIOUS_ACTIVE_ACTIVITY,
38  SERVICE_CHANGE_CHANNEL,
39  SERVICE_SYNC,
40 )
41 from .data import HarmonyConfigEntry, HarmonyData
42 from .entity import HarmonyEntity
43 from .subscriber import HarmonyCallback
44 
45 _LOGGER = logging.getLogger(__name__)
46 
47 # We want to fire remote commands right away
48 PARALLEL_UPDATES = 0
49 
50 ATTR_CHANNEL = "channel"
51 
52 HARMONY_CHANGE_CHANNEL_SCHEMA: VolDictType = {
53  vol.Required(ATTR_CHANNEL): cv.positive_int,
54 }
55 
56 
58  hass: HomeAssistant,
59  entry: HarmonyConfigEntry,
60  async_add_entities: AddEntitiesCallback,
61 ) -> None:
62  """Set up the Harmony config entry."""
63  data = entry.runtime_data
64 
65  _LOGGER.debug("HarmonyData : %s", data)
66 
67  default_activity: str | None = entry.options.get(ATTR_ACTIVITY)
68  delay_secs: float = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
69 
70  harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
71  device = HarmonyRemote(data, default_activity, delay_secs, harmony_conf_file)
72  async_add_entities([device])
73 
74  platform = entity_platform.async_get_current_platform()
75 
76  platform.async_register_entity_service(
77  SERVICE_SYNC,
78  None,
79  "sync",
80  )
81  platform.async_register_entity_service(
82  SERVICE_CHANGE_CHANNEL, HARMONY_CHANGE_CHANNEL_SCHEMA, "change_channel"
83  )
84 
85 
87  """Remote representation used to control a Harmony device."""
88 
89  _attr_supported_features = RemoteEntityFeature.ACTIVITY
90  _attr_name = None
91 
92  def __init__(
93  self, data: HarmonyData, activity: str | None, delay_secs: float, out_path: str
94  ) -> None:
95  """Initialize HarmonyRemote class."""
96  super().__init__(data=data)
97  self._state_state: bool | None = None
98  self._current_activity_current_activity = ACTIVITY_POWER_OFF
99  self.default_activitydefault_activity = activity
100  self._activity_starting_activity_starting = None
101  self._is_initial_update_is_initial_update = True
102  self.delay_secsdelay_secs = delay_secs
103  self._last_activity_last_activity = None
104  self._config_path_config_path = out_path
105  self._attr_unique_id_attr_unique_id = data.unique_id
106  self._attr_device_info_attr_device_info = self._data_data.device_info(DOMAIN)
107 
108  async def _async_update_options(self, data: dict[str, Any]) -> None:
109  """Change options when the options flow does."""
110  if ATTR_DELAY_SECS in data:
111  self.delay_secsdelay_secs = data[ATTR_DELAY_SECS]
112 
113  if ATTR_ACTIVITY in data:
114  self.default_activitydefault_activity = data[ATTR_ACTIVITY]
115 
116  def _setup_callbacks(self) -> None:
117  self.async_on_removeasync_on_remove(
118  self._data_data.async_subscribe(
120  connected=HassJob(self.async_got_connectedasync_got_connected),
121  disconnected=HassJob(self.async_got_disconnectedasync_got_disconnected),
122  config_updated=HassJob(self.async_new_configasync_new_config),
123  activity_starting=HassJob(self.async_new_activityasync_new_activity),
124  activity_started=HassJob(self.async_new_activity_finishedasync_new_activity_finished),
125  )
126  )
127  )
128 
129  @callback
130  def async_new_activity_finished(self, activity_info: tuple) -> None:
131  """Call for finished updated current activity."""
132  self._activity_starting_activity_starting = None
133  self.async_write_ha_stateasync_write_ha_state()
134 
135  async def async_added_to_hass(self) -> None:
136  """Complete the initialization."""
137  await super().async_added_to_hass()
138 
139  _LOGGER.debug("%s: Harmony Hub added", self._data_data.name)
140 
141  self.async_on_removeasync_on_remove(self._async_clear_disconnection_delay_async_clear_disconnection_delay)
142  self._setup_callbacks_setup_callbacks()
143 
144  self.async_on_removeasync_on_remove(
146  self.hasshass,
147  f"{HARMONY_OPTIONS_UPDATE}-{self.unique_id}",
148  self._async_update_options_async_update_options,
149  )
150  )
151 
152  # Store Harmony HUB config, this will also update our current
153  # activity
154  await self.async_new_configasync_new_config()
155 
156  # Restore the last activity so we know
157  # how what to turn on if nothing
158  # is specified
159  if not (last_state := await self.async_get_last_stateasync_get_last_state()):
160  return
161  if ATTR_LAST_ACTIVITY not in last_state.attributes:
162  return
163  if self.is_onis_onis_on:
164  return
165 
166  self._last_activity_last_activity = last_state.attributes[ATTR_LAST_ACTIVITY]
167 
168  @property
169  def current_activity(self):
170  """Return the current activity."""
171  return self._current_activity_current_activity
172 
173  @property
174  def activity_list(self):
175  """Return the available activities."""
176  return self._data_data.activity_names
177 
178  @property
179  def extra_state_attributes(self) -> dict[str, Any]:
180  """Add platform specific attributes."""
181  return {
182  ATTR_ACTIVITY_STARTING: self._activity_starting_activity_starting,
183  ATTR_DEVICES_LIST: self._data_data.device_names,
184  ATTR_LAST_ACTIVITY: self._last_activity_last_activity,
185  }
186 
187  @property
188  def is_on(self) -> bool:
189  """Return False if PowerOff is the current activity, otherwise True."""
190  return self._current_activity_current_activity not in [None, "PowerOff"]
191 
192  @callback
193  def async_new_activity(self, activity_info: tuple) -> None:
194  """Call for updating the current activity."""
195  activity_id, activity_name = activity_info
196  _LOGGER.debug("%s: activity reported as: %s", self._data_data.name, activity_name)
197  self._current_activity_current_activity = activity_name
198  if self._is_initial_update_is_initial_update:
199  self._is_initial_update_is_initial_update = False
200  else:
201  self._activity_starting_activity_starting = activity_name
202  if activity_id != -1:
203  # Save the activity so we can restore
204  # to that activity if none is specified
205  # when turning on
206  self._last_activity_last_activity = activity_name
207  self._state_state = bool(activity_id != -1)
208  self.async_write_ha_stateasync_write_ha_state()
209 
210  async def async_new_config(self, _: dict | None = None) -> None:
211  """Call for updating the current activity."""
212  _LOGGER.debug("%s: configuration has been updated", self._data_data.name)
213  self.async_new_activityasync_new_activity(self._data_data.current_activity)
214  await self.hasshass.async_add_executor_job(self.write_config_filewrite_config_file)
215 
216  async def async_turn_on(self, **kwargs: Any) -> None:
217  """Start an activity from the Harmony device."""
218  _LOGGER.debug("%s: Turn On", self._data_data.name)
219 
220  activity = kwargs.get(ATTR_ACTIVITY, self.default_activitydefault_activity)
221 
222  if not activity or activity == PREVIOUS_ACTIVE_ACTIVITY:
223  if self._last_activity_last_activity:
224  activity = self._last_activity_last_activity
225  elif all_activities := self._data_data.activity_names:
226  activity = all_activities[0]
227 
228  if activity:
229  await self._data_data.async_start_activity(activity)
230  else:
231  _LOGGER.error(
232  "%s: No activity specified with turn_on service", self._data_data.name
233  )
234 
235  async def async_turn_off(self, **kwargs: Any) -> None:
236  """Start the PowerOff activity."""
237  await self._data_data.async_power_off()
238 
239  async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
240  """Send a list of commands to one device."""
241  _LOGGER.debug("%s: Send Command", self._data_data.name)
242  if (device := kwargs.get(ATTR_DEVICE)) is None:
243  _LOGGER.error("%s: Missing required argument: device", self._data_data.name)
244  return
245 
246  num_repeats = kwargs[ATTR_NUM_REPEATS]
247  delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secsdelay_secs)
248  hold_secs = kwargs[ATTR_HOLD_SECS]
249  await self._data_data.async_send_command(
250  command, device, num_repeats, delay_secs, hold_secs
251  )
252 
253  async def change_channel(self, channel: int) -> None:
254  """Change the channel using Harmony remote."""
255  await self._data_data.change_channel(channel)
256 
257  async def sync(self) -> None:
258  """Sync the Harmony device with the web service."""
259  if await self._data_data.sync():
260  await self.hasshass.async_add_executor_job(self.write_config_filewrite_config_file)
261 
262  def write_config_file(self) -> None:
263  """Write Harmony configuration file.
264 
265  This is a handy way for users to figure out the available commands for automations.
266  """
267  _LOGGER.debug(
268  "%s: Writing hub configuration to file: %s",
269  self._data_data.name,
270  self._config_path_config_path,
271  )
272  if (json_config := self._data_data.json_config) is None:
273  _LOGGER.warning("%s: No configuration received from hub", self._data_data.name)
274  return
275 
276  try:
277  with open(self._config_path_config_path, "w+", encoding="utf-8") as file_out:
278  json.dump(json_config, file_out, sort_keys=True, indent=4)
279  except OSError as exc:
280  _LOGGER.error(
281  "%s: Unable to write HUB configuration to %s: %s",
282  self._data_data.name,
283  self._config_path_config_path,
284  exc,
285  )
None async_got_connected(self, str|None _=None)
Definition: entity.py:37
None async_got_disconnected(self, str|None _=None)
Definition: entity.py:44
None __init__(self, HarmonyData data, str|None activity, float delay_secs, str out_path)
Definition: remote.py:94
None async_new_config(self, dict|None _=None)
Definition: remote.py:210
None _async_update_options(self, dict[str, Any] data)
Definition: remote.py:108
None async_send_command(self, Iterable[str] command, **Any kwargs)
Definition: remote.py:239
None async_new_activity_finished(self, tuple activity_info)
Definition: remote.py:130
None async_new_activity(self, tuple activity_info)
Definition: remote.py:193
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
DeviceInfo|None device_info(self)
Definition: entity.py:798
None async_setup_entry(HomeAssistant hass, HarmonyConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: remote.py:61
CALLBACK_TYPE async_subscribe(HomeAssistant hass, str topic, Callable[[ReceiveMessage], Coroutine[Any, Any, None]|None] msg_callback, int qos=DEFAULT_QOS, str|None encoding=DEFAULT_ENCODING)
Definition: client.py:194
None open(self, **Any kwargs)
Definition: lock.py:86
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103