271 lines
9.0 KiB
Python
271 lines
9.0 KiB
Python
"""Support for Harmony Hub devices."""
|
|
import json
|
|
import logging
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components import remote
|
|
from homeassistant.components.remote import (
|
|
ATTR_ACTIVITY,
|
|
ATTR_DELAY_SECS,
|
|
ATTR_DEVICE,
|
|
ATTR_HOLD_SECS,
|
|
ATTR_NUM_REPEATS,
|
|
DEFAULT_DELAY_SECS,
|
|
SUPPORT_ACTIVITY,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant, callback
|
|
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_STARTING,
|
|
ATTR_DEVICES_LIST,
|
|
ATTR_LAST_ACTIVITY,
|
|
DOMAIN,
|
|
HARMONY_DATA,
|
|
HARMONY_OPTIONS_UPDATE,
|
|
PREVIOUS_ACTIVE_ACTIVITY,
|
|
SERVICE_CHANGE_CHANNEL,
|
|
SERVICE_SYNC,
|
|
)
|
|
from .entity import HarmonyEntity
|
|
from .subscriber import HarmonyCallback
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# We want to fire remote commands right away
|
|
PARALLEL_UPDATES = 0
|
|
|
|
ATTR_CHANNEL = "channel"
|
|
|
|
HARMONY_CHANGE_CHANNEL_SCHEMA = {
|
|
vol.Required(ATTR_CHANNEL): cv.positive_int,
|
|
}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
|
):
|
|
"""Set up the Harmony config entry."""
|
|
|
|
data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA]
|
|
|
|
_LOGGER.debug("HarmonyData : %s", data)
|
|
|
|
default_activity = entry.options.get(ATTR_ACTIVITY)
|
|
delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
|
|
|
|
harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
|
|
device = HarmonyRemote(data, default_activity, delay_secs, harmony_conf_file)
|
|
async_add_entities([device])
|
|
|
|
platform = entity_platform.async_get_current_platform()
|
|
|
|
platform.async_register_entity_service(
|
|
SERVICE_SYNC,
|
|
{},
|
|
"sync",
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_CHANGE_CHANNEL, HARMONY_CHANGE_CHANNEL_SCHEMA, "change_channel"
|
|
)
|
|
|
|
|
|
class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity):
|
|
"""Remote representation used to control a Harmony device."""
|
|
|
|
def __init__(self, data, activity, delay_secs, out_path):
|
|
"""Initialize HarmonyRemote class."""
|
|
super().__init__(data=data)
|
|
self._state = None
|
|
self._current_activity = ACTIVITY_POWER_OFF
|
|
self.default_activity = activity
|
|
self._activity_starting = None
|
|
self._is_initial_update = True
|
|
self.delay_secs = delay_secs
|
|
self._last_activity = None
|
|
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
|
|
self._attr_supported_features = SUPPORT_ACTIVITY
|
|
|
|
async def _async_update_options(self, data):
|
|
"""Change options when the options flow does."""
|
|
if ATTR_DELAY_SECS in data:
|
|
self.delay_secs = data[ATTR_DELAY_SECS]
|
|
|
|
if ATTR_ACTIVITY in data:
|
|
self.default_activity = data[ATTR_ACTIVITY]
|
|
|
|
def _setup_callbacks(self):
|
|
callbacks = {
|
|
"connected": self.async_got_connected,
|
|
"disconnected": self.async_got_disconnected,
|
|
"config_updated": self.async_new_config,
|
|
"activity_starting": self.async_new_activity,
|
|
"activity_started": self.async_new_activity_finished,
|
|
}
|
|
|
|
self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks)))
|
|
|
|
@callback
|
|
def async_new_activity_finished(self, activity_info: tuple) -> None:
|
|
"""Call for finished updated current activity."""
|
|
self._activity_starting = None
|
|
self.async_write_ha_state()
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Complete the initialization."""
|
|
await super().async_added_to_hass()
|
|
|
|
_LOGGER.debug("%s: Harmony Hub added", self.name)
|
|
|
|
self.async_on_remove(self._clear_disconnection_delay)
|
|
self._setup_callbacks()
|
|
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{HARMONY_OPTIONS_UPDATE}-{self.unique_id}",
|
|
self._async_update_options,
|
|
)
|
|
)
|
|
|
|
# Store Harmony HUB config, this will also update our current
|
|
# activity
|
|
await self.async_new_config()
|
|
|
|
# Restore the last activity so we know
|
|
# how what to turn on if nothing
|
|
# is specified
|
|
if not (last_state := await self.async_get_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]
|
|
|
|
@property
|
|
def current_activity(self):
|
|
"""Return the current activity."""
|
|
return self._current_activity
|
|
|
|
@property
|
|
def activity_list(self):
|
|
"""Return the available activities."""
|
|
return self._data.activity_names
|
|
|
|
@property
|
|
def extra_state_attributes(self):
|
|
"""Add platform specific attributes."""
|
|
return {
|
|
ATTR_ACTIVITY_STARTING: self._activity_starting,
|
|
ATTR_DEVICES_LIST: self._data.device_names,
|
|
ATTR_LAST_ACTIVITY: self._last_activity,
|
|
}
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return False if PowerOff is the current activity, otherwise True."""
|
|
return self._current_activity not in [None, "PowerOff"]
|
|
|
|
@callback
|
|
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)
|
|
self._current_activity = activity_name
|
|
if self._is_initial_update:
|
|
self._is_initial_update = False
|
|
else:
|
|
self._activity_starting = 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.async_write_ha_state()
|
|
|
|
async def async_new_config(self, _=None):
|
|
"""Call for updating the current activity."""
|
|
_LOGGER.debug("%s: configuration has been updated", self.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):
|
|
"""Start an activity from the Harmony device."""
|
|
_LOGGER.debug("%s: Turn On", self.name)
|
|
|
|
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:
|
|
if all_activities := self._data.activity_names:
|
|
activity = all_activities[0]
|
|
|
|
if activity:
|
|
await self._data.async_start_activity(activity)
|
|
else:
|
|
_LOGGER.error("%s: No activity specified with turn_on service", self.name)
|
|
|
|
async def async_turn_off(self, **kwargs):
|
|
"""Start the PowerOff activity."""
|
|
await self._data.async_power_off()
|
|
|
|
async def async_send_command(self, command, **kwargs):
|
|
"""Send a list of commands to one device."""
|
|
_LOGGER.debug("%s: Send Command", self.name)
|
|
if (device := kwargs.get(ATTR_DEVICE)) is None:
|
|
_LOGGER.error("%s: Missing required argument: device", self.name)
|
|
return
|
|
|
|
num_repeats = kwargs[ATTR_NUM_REPEATS]
|
|
delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs)
|
|
hold_secs = kwargs[ATTR_HOLD_SECS]
|
|
await self._data.async_send_command(
|
|
command, device, num_repeats, delay_secs, hold_secs
|
|
)
|
|
|
|
async def change_channel(self, channel):
|
|
"""Change the channel using Harmony remote."""
|
|
await self._data.change_channel(channel)
|
|
|
|
async def sync(self):
|
|
"""Sync the Harmony device with the web service."""
|
|
if await self._data.sync():
|
|
await self.hass.async_add_executor_job(self.write_config_file)
|
|
|
|
def write_config_file(self):
|
|
"""Write Harmony configuration file.
|
|
|
|
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
|
|
)
|
|
if (json_config := self._data.json_config) is None:
|
|
_LOGGER.warning("%s: No configuration received from hub", self.name)
|
|
return
|
|
|
|
try:
|
|
with open(self._config_path, "w+", encoding="utf-8") as file_out:
|
|
json.dump(json_config, file_out, sort_keys=True, indent=4)
|
|
except OSError as exc:
|
|
_LOGGER.error(
|
|
"%s: Unable to write HUB configuration to %s: %s",
|
|
self.name,
|
|
self._config_path,
|
|
exc,
|
|
)
|