Home Assistant Unofficial Reference 2024.12.1
data.py
Go to the documentation of this file.
1 """Harmony data object which contains the Harmony Client."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 import logging
7 
8 from aioharmony.const import ClientCallbackType, SendCommandDevice
9 import aioharmony.exceptions as aioexc
10 from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.core import HomeAssistant
14 from homeassistant.exceptions import ConfigEntryNotReady
15 from homeassistant.helpers.device_registry import DeviceInfo
16 
17 from .const import ACTIVITY_POWER_OFF
18 from .subscriber import HarmonySubscriberMixin
19 
20 _LOGGER = logging.getLogger(__name__)
21 
22 
23 type HarmonyConfigEntry = ConfigEntry[HarmonyData]
24 
25 
27  """HarmonyData registers for Harmony hub updates."""
28 
29  _client: HarmonyClient
30 
31  def __init__(
32  self, hass: HomeAssistant, address: str, name: str, unique_id: str | None
33  ) -> None:
34  """Initialize a data object."""
35  super().__init__(hass)
36  self.namename = name
37  self._unique_id_unique_id = unique_id
38  self._available_available_available = False
39  self._address_address = address
40 
41  @property
42  def activities(self):
43  """List of all non-poweroff activity objects."""
44  activity_infos = self._client_client.config.get("activity", [])
45  return [
46  info
47  for info in activity_infos
48  if info["label"] is not None and info["label"] != ACTIVITY_POWER_OFF
49  ]
50 
51  @property
52  def activity_names(self) -> list[str]:
53  """Names of all the remotes activities."""
54  activity_infos = self.activitiesactivities
55  return [activity["label"] for activity in activity_infos]
56 
57  @property
58  def device_names(self):
59  """Names of all of the devices connected to the hub."""
60  device_infos = self._client_client.config.get("device", [])
61  return [device["label"] for device in device_infos]
62 
63  @property
64  def unique_id(self):
65  """Return the Harmony device's unique_id."""
66  return self._unique_id_unique_id
67 
68  @property
69  def json_config(self):
70  """Return the hub config as json."""
71  if self._client_client.config is None:
72  return None
73  return self._client_client.json_config
74 
75  @property
76  def available(self) -> bool:
77  """Return if connected to the hub."""
78  return self._available_available_available
79 
80  @property
81  def current_activity(self) -> tuple:
82  """Return the current activity tuple."""
83  return self._client_client.current_activity
84 
85  def device_info(self, domain: str) -> DeviceInfo:
86  """Return hub device info."""
87  model = "Harmony Hub"
88  if "ethernetStatus" in self._client_client.hub_config.info:
89  model = "Harmony Hub Pro 2400"
90  return DeviceInfo(
91  identifiers={(domain, self.unique_idunique_id)},
92  manufacturer="Logitech",
93  model=model,
94  name=self.namename,
95  sw_version=self._client_client.hub_config.info.get(
96  "hubSwVersion", self._client_client.fw_version
97  ),
98  configuration_url="https://www.logitech.com/en-us/my-account",
99  )
100 
101  async def connect(self) -> None:
102  """Connect to the Harmony Hub."""
103  _LOGGER.debug("%s: Connecting", self.namename)
104 
105  callbacks = {
106  "config_updated": self._config_updated_config_updated,
107  "connect": self._connected_connected,
108  "disconnect": self._disconnected_disconnected,
109  "new_activity_starting": self._activity_starting_activity_starting,
110  "new_activity": self._activity_started_activity_started,
111  }
112  self._client_client = HarmonyClient(
113  ip_address=self._address_address, callbacks=ClientCallbackType(**callbacks)
114  )
115 
116  connected = False
117  try:
118  connected = await self._client_client.connect()
119  except (TimeoutError, aioexc.TimeOut) as err:
120  await self._client_client.close()
121  raise ConfigEntryNotReady(
122  f"{self.name}: Connection timed-out to {self._address}:8088"
123  ) from err
124  except (ValueError, AttributeError) as err:
125  await self._client_client.close()
126  raise ConfigEntryNotReady(
127  f"{self.name}: Error {err} while connected HUB at:"
128  f" {self._address}:8088"
129  ) from err
130  if not connected:
131  await self._client_client.close()
132  raise ConfigEntryNotReady(
133  f"{self.name}: Unable to connect to HUB at: {self._address}:8088"
134  )
135 
136  async def shutdown(self) -> None:
137  """Close connection on shutdown."""
138  _LOGGER.debug("%s: Closing Harmony Hub", self.namename)
139  try:
140  await self._client_client.close()
141  except aioexc.TimeOut:
142  _LOGGER.warning("%s: Disconnect timed-out", self.namename)
143 
144  async def async_start_activity(self, activity: str) -> None:
145  """Start an activity from the Harmony device."""
146 
147  if not activity:
148  _LOGGER.error("%s: No activity specified with turn_on service", self.namename)
149  return
150 
151  activity_id = None
152  activity_name = None
153 
154  if activity.isdigit() or activity == "-1":
155  _LOGGER.debug("%s: Activity is numeric", self.namename)
156  activity_name = self._client_client.get_activity_name(int(activity))
157  if activity_name:
158  activity_id = activity
159 
160  if activity_id is None:
161  _LOGGER.debug("%s: Find activity ID based on name", self.namename)
162  activity_name = str(activity)
163  activity_id = self._client_client.get_activity_id(activity_name)
164 
165  if activity_id is None:
166  _LOGGER.error("%s: Activity %s is invalid", self.namename, activity)
167  return
168 
169  _, current_activity_name = self.current_activitycurrent_activity
170  if current_activity_name == activity_name:
171  # Automations or HomeKit may turn the device on multiple times
172  # when the current activity is already active which will cause
173  # harmony to loose state. This behavior is unexpected as turning
174  # the device on when its already on isn't expected to reset state.
175  _LOGGER.debug(
176  "%s: Current activity is already %s", self.namename, activity_name
177  )
178  return
179 
180  await self.async_lock_start_activityasync_lock_start_activity()
181  try:
182  await self._client_client.start_activity(activity_id)
183  except aioexc.TimeOut:
184  _LOGGER.error("%s: Starting activity %s timed-out", self.namename, activity)
185  self.async_unlock_start_activityasync_unlock_start_activity()
186 
187  async def async_power_off(self) -> None:
188  """Start the PowerOff activity."""
189  _LOGGER.debug("%s: Turn Off", self.namename)
190  try:
191  await self._client_client.power_off()
192  except aioexc.TimeOut:
193  _LOGGER.error("%s: Powering off timed-out", self.namename)
194 
196  self,
197  commands: Iterable[str],
198  device: str,
199  num_repeats: int,
200  delay_secs: float,
201  hold_secs: float,
202  ) -> None:
203  """Send a list of commands to one device."""
204  device_id = None
205  if device.isdigit():
206  _LOGGER.debug("%s: Device %s is numeric", self.namename, device)
207  if self._client_client.get_device_name(int(device)):
208  device_id = device
209 
210  if device_id is None:
211  _LOGGER.debug(
212  "%s: Find device ID %s based on device name", self.namename, device
213  )
214  device_id = self._client_client.get_device_id(str(device).strip())
215 
216  if device_id is None:
217  _LOGGER.error("%s: Device %s is invalid", self.namename, device)
218  return
219 
220  _LOGGER.debug(
221  (
222  "Sending commands to device %s holding for %s seconds "
223  "with a delay of %s seconds"
224  ),
225  device,
226  hold_secs,
227  delay_secs,
228  )
229 
230  # Creating list of commands to send.
231  snd_cmnd_list = []
232  for _ in range(num_repeats):
233  for single_command in commands:
234  send_command = SendCommandDevice(
235  device=device_id, command=single_command, delay=hold_secs
236  )
237  snd_cmnd_list.append(send_command)
238  if delay_secs > 0:
239  snd_cmnd_list.append(float(delay_secs))
240 
241  _LOGGER.debug("%s: Sending commands", self.namename)
242  try:
243  result_list = await self._client_client.send_commands(snd_cmnd_list)
244  except aioexc.TimeOut:
245  _LOGGER.error("%s: Sending commands timed-out", self.namename)
246  return
247 
248  for result in result_list:
249  _LOGGER.error(
250  "Sending command %s to device %s failed with code %s: %s",
251  result.command.command,
252  result.command.device,
253  result.code,
254  result.msg,
255  )
256 
257  async def change_channel(self, channel: int) -> None:
258  """Change the channel using Harmony remote."""
259  _LOGGER.debug("%s: Changing channel to %s", self.namename, channel)
260  try:
261  await self._client_client.change_channel(channel)
262  except aioexc.TimeOut:
263  _LOGGER.error("%s: Changing channel to %s timed-out", self.namename, channel)
264 
265  async def sync(self) -> bool:
266  """Sync the Harmony device with the web service.
267 
268  Returns True if the sync was successful.
269  """
270  _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.namename)
271  try:
272  await self._client_client.sync()
273  except aioexc.TimeOut:
274  _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.namename)
275  return False
276 
277  return True
None async_send_command(self, Iterable[str] commands, str device, int num_repeats, float delay_secs, float hold_secs)
Definition: data.py:202
None async_start_activity(self, str activity)
Definition: data.py:144
None __init__(self, HomeAssistant hass, str address, str name, str|None unique_id)
Definition: data.py:33
None change_channel(self, int channel)
Definition: data.py:257
DeviceInfo device_info(self, str domain)
Definition: data.py:85
str get_device_id(ServerInfoMessage server_info, MatterEndpoint endpoint)
Definition: helpers.py:59