"""Harmony data object which contains the Harmony Client.""" from __future__ import annotations from collections.abc import Iterable import logging from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient from .const import ACTIVITY_POWER_OFF from .subscriber import HarmonySubscriberMixin _LOGGER = logging.getLogger(__name__) class HarmonyData(HarmonySubscriberMixin): """HarmonyData registers for Harmony hub updates.""" def __init__(self, hass, address: str, name: str, unique_id: str): """Initialize a data object.""" super().__init__(hass) self._name = name self._unique_id = unique_id self._available = False self._client = None self._address = address @property def activities(self): """List of all non-poweroff activity objects.""" activity_infos = self._client.config.get("activity", []) return [ info for info in activity_infos if info["label"] is not None and info["label"] != ACTIVITY_POWER_OFF ] @property def activity_names(self): """Names of all the remotes activities.""" activity_infos = self.activities activities = [activity["label"] for activity in activity_infos] return activities @property def device_names(self): """Names of all of the devices connected to the hub.""" device_infos = self._client.config.get("device", []) devices = [device["label"] for device in device_infos] return devices @property def name(self): """Return the Harmony device's name.""" return self._name @property def unique_id(self): """Return the Harmony device's unique_id.""" return self._unique_id @property def json_config(self): """Return the hub config as json.""" if self._client.config is None: return None return self._client.json_config @property def available(self) -> bool: """Return if connected to the hub.""" return self._available @property def current_activity(self) -> tuple: """Return the current activity tuple.""" return self._client.current_activity def device_info(self, domain: str): """Return hub device info.""" model = "Harmony Hub" if "ethernetStatus" in self._client.hub_config.info: model = "Harmony Hub Pro 2400" return { "identifiers": {(domain, self.unique_id)}, "manufacturer": "Logitech", "sw_version": self._client.hub_config.info.get( "hubSwVersion", self._client.fw_version ), "name": self.name, "model": model, } async def connect(self) -> bool: """Connect to the Harmony Hub.""" _LOGGER.debug("%s: Connecting", self._name) callbacks = { "config_updated": self._config_updated, "connect": self._connected, "disconnect": self._disconnected, "new_activity_starting": self._activity_starting, "new_activity": self._activity_started, } self._client = HarmonyClient( ip_address=self._address, callbacks=ClientCallbackType(**callbacks) ) try: if not await self._client.connect(): _LOGGER.warning("%s: Unable to connect to HUB", self._name) await self._client.close() return False except aioexc.TimeOut: _LOGGER.warning("%s: Connection timed-out", self._name) return False return True async def shutdown(self): """Close connection on shutdown.""" _LOGGER.debug("%s: Closing Harmony Hub", self._name) try: await self._client.close() except aioexc.TimeOut: _LOGGER.warning("%s: Disconnect timed-out", self._name) async def async_start_activity(self, activity: str): """Start an activity from the Harmony device.""" if not activity: _LOGGER.error("%s: No activity specified with turn_on service", self.name) return activity_id = None activity_name = None if activity.isdigit() or activity == "-1": _LOGGER.debug("%s: Activity is numeric", self.name) activity_name = self._client.get_activity_name(int(activity)) if activity_name: activity_id = activity if activity_id is None: _LOGGER.debug("%s: Find activity ID based on name", self.name) activity_name = str(activity) activity_id = self._client.get_activity_id(activity_name) if activity_id is None: _LOGGER.error("%s: Activity %s is invalid", self.name, activity) return _, current_activity_name = self.current_activity if current_activity_name == activity_name: # Automations or HomeKit may turn the device on multiple times # when the current activity is already active which will cause # harmony to loose state. This behavior is unexpected as turning # the device on when its already on isn't expected to reset state. _LOGGER.debug( "%s: Current activity is already %s", self.name, activity_name ) return await self.async_lock_start_activity() try: await self._client.start_activity(activity_id) except aioexc.TimeOut: _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) self.async_unlock_start_activity() async def async_power_off(self): """Start the PowerOff activity.""" _LOGGER.debug("%s: Turn Off", self.name) try: await self._client.power_off() except aioexc.TimeOut: _LOGGER.error("%s: Powering off timed-out", self.name) async def async_send_command( self, commands: Iterable[str], device: str, num_repeats: int, delay_secs: float, hold_secs: float, ): """Send a list of commands to one device.""" device_id = None if device.isdigit(): _LOGGER.debug("%s: Device %s is numeric", self.name, device) if self._client.get_device_name(int(device)): device_id = device if device_id is None: _LOGGER.debug( "%s: Find device ID %s based on device name", self.name, device ) device_id = self._client.get_device_id(str(device).strip()) if device_id is None: _LOGGER.error("%s: Device %s is invalid", self.name, device) return _LOGGER.debug( "Sending commands to device %s holding for %s seconds " "with a delay of %s seconds", device, hold_secs, delay_secs, ) # Creating list of commands to send. snd_cmnd_list = [] for _ in range(num_repeats): for single_command in commands: send_command = SendCommandDevice( device=device_id, command=single_command, delay=hold_secs ) snd_cmnd_list.append(send_command) if delay_secs > 0: snd_cmnd_list.append(float(delay_secs)) _LOGGER.debug("%s: Sending commands", self.name) try: result_list = await self._client.send_commands(snd_cmnd_list) except aioexc.TimeOut: _LOGGER.error("%s: Sending commands timed-out", self.name) return for result in result_list: _LOGGER.error( "Sending command %s to device %s failed with code %s: %s", result.command.command, result.command.device, result.code, result.msg, ) async def change_channel(self, channel: int): """Change the channel using Harmony remote.""" _LOGGER.debug("%s: Changing channel to %s", self.name, channel) try: await self._client.change_channel(channel) except aioexc.TimeOut: _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel) async def sync(self) -> bool: """Sync the Harmony device with the web service. Returns True if the sync was successful. """ _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) try: await self._client.sync() except aioexc.TimeOut: _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) return False else: return True