I’ve always been disappointed with the integration with harmony and home assistant. The source of my disappointment has been with the harmony hub itself and how it behaves.
When using the physical harmony remote, activating an activity will turn off other activities. That means whatever control we use in home assistant will need to monitor state changes that come from the physical remote. Also, the only way to properly have this behavior represented in home assistant is with a input_select. Unfortunately, input_selects do not work with emulated hue.
The solution I came up with is to treat a series of toggle buttons as “radio buttons”. If you are unfamiliar with radio buttons, they are buttons where only 1 button can be on in a series of buttons or all buttons can be off.
I came up with a solution that sort of worked using automations. This required me to have 4 automations, and 1 script for each input _boolean. Also, I had to create a sensor to monitor all the input_booleans states. To say it was cumbersome is an understatement. Adding a new activity was painful.
So I came up with this AppDaemon solution. Hopefully this will help someone else.
with your current config, create input_booleans that match up with activities. Skip the PowerOff activity.
Activities in config:
Activities
30947237 - TV
24089909 - PS4
24103421 - Xbox One
-1 - PowerOff
create the following input booleans:
tv:
initial: off
ps4:
initial: off
xbox_one:
initial: off
Then use the following code for an app:
import appdaemon.appapi as appapi
import os
HARMONY_REMOTE = "remote.harmony_hub"
HARMONY_CONFIG = r"/config/harmony_{0}.conf".format(HARMONY_REMOTE.split(".")[-1])
DEBUG = "DEBUG"
class HarmonyBase(object):
def __init__(self, line):
items = [ s.strip() for s in line.split(" - ") ]
self.id = ""
self.name = ""
if len(items) == 2:
self.id, self.name = items
def _is_valid(self):return True if self.id and self.name else False
is_valid = property(_is_valid, None)
class HarmonyActivity(HarmonyBase):
def __init__(self, line):
HarmonyBase.__init__(self, line)
self._bool_name = self.name.replace(" ", "_").lower() # replace spaces with underscore and make it lowercase to match home assistant.
def _is_power(self): return self.name == "PowerOff"
is_power = property(_is_power, None)
def _get_boolname(self, sep): return "input_boolean{0}{1}".format(sep, self._bool_name) if not self._is_power() else ""
def _get_entity(self): return self._get_boolname(".")
entity = property(_get_entity, None)
def _get_path(self): return self._get_boolname(".")
path = property(_get_path, None)
class HarmonyDevice(HarmonyBase):
def __init__(self, line, commands):
HarmonyBase.__init__(self, line)
self.commands = commands
class HarmonyConfig(object):
def __init__(self, config_file, remote):
with open(config_file, 'r') as f:
content = f.readlines()
self.remote = remote
self.activities = []
self.power_off = None
self.devices = []
self._current_section = ""
self._current_sub_section = ""
self._content = {}
self._parse_content(content)
def _is_valid(self):
# Verifies all information is good from the parsed file
valid_activities = all(activity.is_valid for activity in self.activities)
valid_devices = all(device.is_valid for device in self.devices)
#return ( valid_activities and valid_devices and self.power_off.is_valid )
return self.power_off.is_valid
is_valid = property(_is_valid, None)
def _parse_content(self, content):
for line in content:
meat = line.strip()
whitespace = len(line)-len(line.lstrip(' '))
if meat:
if whitespace == 0:
self._add_section(meat) # add make an activities or device container
elif whitespace == 2:
self._add_to_section(meat) # add the activity or device to the correct container.
elif whitespace == 4:
self._add_command(meat) # add the command to the correct activity or device.
else:
# shouldn't get here, ignore.
continue
else:
# empty line, ignore.
continue
for section_name, section in self._content.items():
if "Activities" in section_name:
for line, commands in section.items():
activity = HarmonyActivity(line)
if activity.is_power:
self.power_off = activity # we found the power off activity
else:
self.activities.append(activity) # we found something other than power off.
if "Device" in section_name:
for line, commands in section.items():
self.devices.append(HarmonyDevice(line, commands)) # add the device to the object.
def _has_curent_section(self): return self._current_section and self._current_section in self._content.keys()
def _has_current_sub_section(self):
if self._has_curent_section(): # if we have the current section in out content
return self._current_sub_section and self._current_sub_section in self._content[self._current_section].keys() # if we have the current sub section.
else:
return False # we don't have the current section.
def _add_section(self, section):
self._current_section = section
self._content[section] = {}
def _add_to_section(self, sub_section):
if self._has_curent_section(): # if we have the current section in out content
self._current_sub_section = sub_section # update the current sub section
self._content[self._current_section][self._current_sub_section] = [] # make a list of commands
def _add_command(self, command):
if self._has_current_sub_section():
self._content[self._current_section][self._current_sub_section].append(command) # add the command.
class ManageActivities(appapi.AppDaemon):
def initialize(self):
self.harmony = harmony = HarmonyConfig(HARMONY_CONFIG, HARMONY_REMOTE)
if harmony.is_valid: #make the buttons work if we have a valid config, otherwise don't.
self.log("Valid Config: {0}".format(HARMONY_CONFIG), level="INFO")
#listeners for each activity input_boolean.
for activity in harmony.activities:
self.log("listening to: {0}".format(activity.entity), level=DEBUG)
self.listen_state(self.track_activity_toggle, entity = activity.entity)
#listener for the state of the harmony activity.
self.listen_state(self.track_harmony_activity, entity = harmony.remote, attribute = "current_activity")
else:
self.error("Invalid Config! {0}".format(HARMONY_CONFIG))
def _get_stated_activities(self, current_activity, state):
""" get all activities but the current one that match the provided state """
return [ activity for activity in self.harmony.activities if activity.entity != current_activity and self.get_state(activity.entity) == state ]
def get_active_activities(self, current_activity):
""" gets all activities that are on """
return self._get_stated_activities(current_activity, "on") #get all activities that are on
def get_inactive_activities(self, current_activity):
""" gets all activities that are off, may not be needed. """
return self._get_stated_activities(current_activity, "off") #get all activities that are off
def get_activity_from_entity(self, which):
""" gets the provided activity from the entity id """
for activity in self.harmony.activities:
if activity.entity == which:
return activity
return None
def get_activity_from_friendly_name(self, name):
""" gets the provided activity from the friendly name from harmony """
for activity in self.harmony.activities:
if activity.name == name:
return activity
return None
def track_harmony_activity(self, entity, attribute, old, new, kwargs):
""" track the toggle button for this activity"""
activity = self.get_activity_from_friendly_name(new)
if activity: #if we have an activity
if self.get_state(activity.entity) == "off":
self.turn_on(activity.entity)
def track_activity_toggle(self, entity, attribute, old, new, kwargs):
""" tracks an activities toggle button. """
self.log("{0} -> {2}".format(entity, old, new), level=DEBUG)
if old == "off" and new == "on":
# Turning toggled activity on
self.turn_on_harmony_activity(entity) # Comment this out if you are using the self.sun_down()
# Uncomment this section to add other automations when the activity is turned on.
# if self.sun_down():
# # if the sun is below the horizon.
# self.turn_on( "light.living_room_d_level", brightness = 255 ) # turn on living room light to full
# # turn on the harmony activity and sync the buttons with the activities states.
# self.turn_on_harmony_activity(entity)
# else:
# # turn on the harmony activity and sync the buttons with the activities states.
# self.turn_on_harmony_activity(entity)
elif old == "on" and new == "off":
# Turning toggled activity off.
# This will not actually turn off the other activities because the harmony remote will do that natively.
# This is the whole reason this needed to be handled this way.
activities = self.get_active_activities(entity) # get any other toggles that are on.
if len(activities) == 0: # if no other activities are currently on...
self._turn_on_harmony_activity(self.harmony.power_off) #turn off all activities
else:
# we shouldn't ever get here because the states should always be on & off or off & on.
self.error("State change was unexpected!")
def turn_on_harmony_activity(self, which):
activity = self.get_activity_from_entity(which) # get the current activity to turn on.
if activity: # if we have one...
self._turn_on_harmony_activity(activity) #turn on. made a separate function to play around.
# Due to delays from the harmony polling, this log may not look correct when output.
self.log(self.get_state("remote.living_room", "all"), level=DEBUG)
# Find all the toggles that are on and sync them up with the physical harmony remote.
for active_activity in self.get_active_activities(activity.entity): #iterate through toggles that are currently on
self.log("Syncing {0}".format(active_activity.entity), level=DEBUG)
self.turn_off(active_activity.entity)
def _turn_on_harmony_activity(self, activity):
self.turn_on("remote.living_room", activity = activity.name)
#self.call_service("remote/turn_on", entity_id = "remote/living_room", activity = activity.id) # turn on living room light to full
It’s not perfect, but it creates a series of toggles that behave like radio buttons. The buttons update with the state of the harmony remote if its used outside homeassistant.