Setting up OpenHAB as a HomeKit bridge for Logitech Harmony Hub

OpenHAB is a great platform!!!
Unfortunately, does require a bit of tinkering if you want it to do slightly more than your regular stuff…
I personally have struggled quite a bit with my Harmony Hub systems. Not necessarily getting it to identify the Hubs in OpenHAB, this part has actually been greatly improved and is quite seamless, however, when I wanted to get it to work with my Apple Homekit, that’s where it became more complicated.

The biggest pain in the @$$ is making sure it communicates in both directions. Meaning whether you initiated starting your TV from Homekit or from the Harmony remote/app, it will reflect back in the Apple Home app.
Now, unlike OH, that fully supports a dropdown list to choose which activity you want to start from the available ones, Homekit is unablke to do so… hence the only (current) solution is to have each activity show up as its own Switch. So in my example I will show using two activities that I have “TV” and “Movies”.

My Setup:

  • I have OH installed on a Raspberry Pi 3 with the latest Openhabian (at the time of writing this tutorial, 1.4.1).
  • I have 2 Harmony Hubs. 1 in the Living room and one in the Master bedroom (I want to control both).
    ** Both Hubs have an STB for regular TV channel viewing and an Amazon Fire TV with Kodi on it for streaming.
  • I am using Apple Homekit for all my smart home devices and wanted to control my entertainment as well with it.
    ** Apple homekit has some difficulties getting voice commands for certain items. Therefore, to make it easy for me to simply say “Hey Siri, turn on …” I changed the Activity names in the Logitech Harmony app before starting the OH configuration.

I definitely recommend having the control setup fully configured within the Harmony. It will make your life a lot easier by just interacting with the Activities and letting Harmony take care of the rest.
I renamed the default “Watch TV” activity to simply “TV”. Also renamed the “Fire TV” activity to “Movies”. And since I have two hubs, one in the Living room and one in the Master bedroom, once I located the correct Homekit switches I created with OH, I am now able to simply say “Hey Siri, turn on Living room TV” or “Hey Siri, turn on Master bedroom Movies”.

Assumption: I am assuming that you have already installed the Homekit integration for OH (‘Paper UI -> Add-ons -> Misc -> HomeKit Integration’) and added OH as a bridge manually to your Apple Home app. If not, please refer to: https://docs.openhab.org/addons/ios/homekit/readme.html

So, once I had my Harmony side updated I set up OH.
First, add the OH Harmony binding in ‘Paper UI -> Add-ons -> Bindings -> Harmony Hub Binding’. Once installed, Go to your ‘Paper UI -> Inbox’ to scan for your hubs. OH does a very nice job identifying all your home hubs (within the same network) along with all configured devices (your TVs, STBs, Receivers…).
On the identified list, I only added the Harmony Hubs (clicking the check mark for the Hub things) to my managed OH Things, again, because I am letting Harmony take care of the ON and OFF automation. Now I have my Hubs under ‘Paper UI -> Configuration -> Things’:


As you can see, I also renamed the Hub names in advance in the Harmony setup app. Just to it simple to differentiate between the two.
Clicking on one of them will also show you the supported triggers for each one (ignore the Current Activity with a different color, this is because I have it set up already):

Please note the full Identifier name for Current Activity! We will need this when we define it in the items section.

From here moving on we will configure the files manually through SSH. These files are located under ‘/etc/openhab2/’. There is a folder for each file we will need: items, rules. If this is your first time setting these up I definitely recommend going through the OH beginners guide.

My items file:

// Logitech Harmony Hub - Living Room
Switch LivingRoom_TV "TV" [ "Switchable" ]
Switch LivingRoom_Movies "Movies" [ "Switchable" ]
String LivingRoomHub "Current Activity [%s]" { channel="harmonyhub:hub:LivingRoom:currentActivity" }

// Logitech Harmony Hub - Master Bedroom
Switch MasterBedroom_TV "TV" [ "Switchable" ]
Switch MasterBedroom_Movies "Movies" [ "Switchable" ]
String MasterBedroomHub "Current Activity [%s]" { channel="harmonyhub:hub:MasterBedroom:currentActivity" }

I have two Harmony hubs as mentioned. The first two items for each are simply the switches that are then exposed to Homekit.

  • Switch :Designates this item to be a Switch item (ON/OFF)
  • LivingRoom_TV :The OH name for the item. Can be whatever you want it to be
  • "TV" :The name of the switch as it will show up in Homekit and OH
  • [ "Switchable" ] :Exposes this item to Homekit

The third item on each creates a linked item between on OH item and the current running activity on the Harmony Hub. This is not exposed to Homekit, as I mentioned previously it does not support it. We do need this item later as we create rules in OH to identify the status of the Hub to match the switches.

  • String :Designates this item to be of String type (as it has the activity names as strings)
  • LivingRoomHub :The OH name for the item. Can be whatever you want it to be
  • "Current Activity [%s]" :Used to report and update the current activity of the OH item
  • { channel="harmonyhub:hub:LivingRoom:currentActivity" } :Connects the OH item to the correct channel (refer to my previous comment regarding the Current Activity property identifier)

Now that we have our items properly configured, we can move to setting up the rules. Here is my rules file:

rule "Starting Values"
        when
                System started
        then
                if ( LivingRoom_TV.state == NULL ) { postUpdate(LivingRoom_TV,OFF) }
                if ( LivingRoom_Movies.state == NULL ) { postUpdate(LivingRoom_Movies,OFF) }
                if ( MasterBedroom_TV.state == NULL ) { postUpdate(MasterBedroom_TV,OFF) }
                if ( MasterBedroom_Movies.state == NULL ) { postUpdate(MasterBedroom_Movies,OFF) }
end

rule "Catch Living Room Hub State for HomeKit"
        when
                Item LivingRoomHub changed
        then
                switch (LivingRoomHub.state) {
                        case "PowerOff" : {
                                postUpdate(LivingRoom_TV,OFF)
                                postUpdate(LivingRoom_Movies,OFF)
                        }
                        case "Movies" : {
                                postUpdate(LivingRoom_TV,OFF)
                                postUpdate(LivingRoom_Movies,ON)
                        }
                        case "TV" : {
                                postUpdate(LivingRoom_TV,ON)
                                postUpdate(LivingRoom_Movies,OFF)
                        }
                }
end

rule "Catch Master Bedroom Hub State for HomeKit"
        when
                Item MasterBedroomHub changed
        then
                switch (MasterBedroomHub.state) {
                        case "PowerOff" : {
                                postUpdate(MasterBedroom_TV,OFF)
                                postUpdate(MasterBedroom_Movies,OFF)
                        }
                        case "Movies" : {
                                postUpdate(MasterBedroom_TV,OFF)
                                postUpdate(MasterBedroom_Movies,ON)
                        }
                        case "TV" : {
                                postUpdate(MasterBedroom_TV,ON)
                                postUpdate(MasterBedroom_Movies,OFF)
                        }
                }
end

rule "Living Room TV Start"
        when
                Item LivingRoom_TV changed from OFF to ON
        then
                if (LivingRoomHub.state != "TV")
                {
                        LivingRoomHub.sendCommand("TV")
                }
                postUpdate(LivingRoom_Movies,OFF)

end

rule "Living Room Movies Start"
        when
                Item LivingRoom_Movies changed from OFF to ON
        then
                if (LivingRoomHub.state != "Movies")
                {
                        LivingRoomHub.sendCommand("Movies")
                }
                postUpdate(LivingRoom_TV,OFF)
end

rule "Master Bedroom TV Start"
        when
                Item MasterBedroom_TV changed from OFF to ON
        then
                if (MasterBedroomHub.state != "TV")
                {
                        MasterBedroomHub.sendCommand("TV")
                }
                postUpdate(MasterBedroom_Movies,OFF)

end

rule "Master Bedroom Movies Start"
        when
                Item MasterBedroom_Movies changed from OFF to ON
        then
                if (MasterBedroomHub.state != "Movies")
                {
                        MasterBedroomHub.sendCommand("Movies")
                }
                postUpdate(MasterBedroom_TV,OFF)
end

rule "When all Living Room devices are off"
        when
                Item LivingRoom_TV changed from ON to OFF or
                Item LivingRoom_Movies changed from ON to OFF
        then
                Thread::sleep(250)
                if (LivingRoomHub.state != "PowerOff")
                {
                        if (LivingRoomHub.state == "TV" && LivingRoom_TV.state == OFF) {
                                LivingRoomHub.sendCommand("PowerOff")
                        }
                        if (LivingRoomHub.state == "Movies" && LivingRoom_Movies.state == OFF) {
                                LivingRoomHub.sendCommand("PowerOff")
                        }
                }
end

rule "When all MasterBedroom devices are off"
        when
                Item MasterBedroom_TV changed from ON to OFF or
                Item MasterBedroom_Movies changed from ON to OFF
        then
                Thread::sleep(250)
                if (MasterBedroomHub.state != "PowerOff")
                {
                        if (MasterBedroomHub.state == "TV" && MasterBedroom_TV.state == OFF) {
                                MasterBedroomHub.sendCommand("PowerOff")
                        }
                        if (MasterBedroomHub.state == "Movies" && MasterBedroom_Movies.state == OFF) {
                                MasterBedroomHub.sendCommand("PowerOff")
                        }
                }
end

Now I’ll try to simplify it as much as I can.
First section just set the initial values of the switches to OFF if they are not set already when the system boots up for both switches on both hubs.
The rest is divided between Living room rules and Master bedroom rules, but they are the same. You can obviously play with these rules by removing a hub if you only have one and/or adding additional activities if you have more.
The rule “Catch Living Room Hub State for HomeKit” looks for changes happening on the Hub itself and then uses these changes to reflect back on the OH switches, which then reflect these changes in HomeKit… :wink:
Note that I am using the postUpdate and not sendCommand option since I just need the switches to reflect this update and not actually to have the switch make a change on the hub.
The subsequent rules for start and activity using the switches monitor for each specific switch if it changed from OFF to ON, meaning it was either pressed in the OH web and/or app UI (exposing the item in a sitemap) or pressed ON using the Apple Home app (because we used the [ "Switchable" ] option with the items). The rule then checks if the relevant hub is potentially already running this activity. If not, it will tell the hub using sendCommand to switch to that activity. It will also update the other switch to reflect the fact that it is no longer running that activity using the postUpdate command.
For example; You have your Harmony Hub currently on ‘TV’. So, currently you have the ‘TV’ switch on ON and your ‘Movies’ switch on OFF. Flipping the ‘Movies’ switch to ON will do two things:

  1. Send a command to the Harmony Hub to tell it to switch the the Movies activity.
  2. Will update the OH/Apple Home TV switch to reflect an OFF state.

Last but not least, switching to OFF using the OH/Apple Home switches.
Since up till now we made sure that if you have two activities (can obviously be more) and you flipped one ON, Our rules will flip the other switches OFF, if you actually flip the only remaining activity the is ON to OFF it means that you want to power off your entertainment system. The last rules refer to all devices off.
First, I check if any of the corresponding switches for that hub have changed from ON to OFF. If so, after a short delay I validate if the current Hub state is powered off. If not, for each switch I validate if that hub is running the specific activity that its OH item if currently switched to OFF. If that is the case then it issues the PowerOFF command to the Hub.

I did not include a description about exposing the items in OH as well, as I am using OH in this specific case as a HomeKit bridge, therefore I have no need to expose them in the sitemap. However, for those who want that as well, there is a very good sitemap example at the bottom of the binding help page: https://docs.openhab.org/addons/bindings/harmonyhub/readme.html

This is it!!! I truly hope this helps anyone else struggling to get this working (I know I did…).
Good luck!

4 Likes

Thanks for the tutorial. I was looking to do this exact thing (regarding getting some HomeKit integration), and your post was really helpful in getting me pointed in the right direction.

With the release of OpenHAB 2.3, we gained the ability to use "Member of " as a rule trigger, and I’ve been really focused on trying to leverage that. With that in mind, I’ve taken some steps to streamline your rules, and make them more scalable (as you add or remove activities from your Harmony configuration, you won’t have to touch the rules).

Here’s a general overview of how it works:

  1. Each hub, and the switches associated with that hub’s activities must be placed into a group.

  2. A sync method that accepts a group, and that group’s current activity as a string. The method then loops through all the switches associated with that group, and sets them ON or OFF accordingly.

  3. A system start-up rule that runs the sync method against an array of Harmony groups (if you have more than one hub and activities).

  4. An event rule with one or more groups as the event trigger. This way, the rule fires on a switch change, or the hub receiving a command via the remote or any other method. If it’s fired from a switch, it will send the appropriate command to the group’s hub. I determine the hub by searching the group for the first item that is NOT a switch. Then, the sync method is called to update the switch states.

Caveats:

I only have one hub and group, but in theory, it should work with multiple hubs. Each hub (and associated switches) would have to have its own group name. You must update the harmonyGroups array on lines 31 and 45 with your harmony group names. Since there is no way to get a group object from the triggeringItem, I implemented this as a work-around. The code loops through each group, looking for a member item that matches the triggeringItem.

Also, I have my items file defined such that the activities’ labels are “TV” and “PC”. The name of the Harmony activities are “Watch TV” and “Watch PC” accordingly. So, the rule is written to prepend the "Watch " value to the item label before it does the comparison. You might have to change this comparison to match your environment. (Lines 11 and 68)

Here are the full contents of my harmony.items file:

// Logitech Harmony Hub - Family Room
//Label must match name of Harmony activity without the "Watch".  Eg: "Watch TV" -> "TV"
Switch Harmony_FamilyRoom_TV "TV"   (g_Harmony) [ "Switchable" ]
Switch Harmony_FamilyRoom_PC "PC" (g_Harmony) [ "Switchable" ]
String Harmony_FamilyRoom_Activity "Current Activity [%s]"  (g_Harmony) { channel="harmonyhub:hub:HarmonyHub:currentActivity" }

Here are the full contents of my harmony.rules file:

import org.eclipse.xtext.xbase.lib.Procedures
import java.util.List

val logName = "rules"
val fileName = "harmony.rules"

val Procedures$Procedure4<String, String, GroupItem, String> syncHarmonySwitchesWithHubState = [ logName, fileName, triggerGroup, hubCommand |
    try {
        triggerGroup.members.filter[ hm | hm.type == "Switch"].forEach[ hm | 
            var targetState = OFF
            if (hubCommand == "Watch " + hm.label)
            {
                targetState = ON
            }

            if (hm.state != targetState) //switch state out of sync
            {
                hm.postUpdate(targetState)
                logInfo(logName, "{} - Out of sync harmony switch item {} updated to {}}", fileName, hm.name, targetState)
            }
        ]
    } catch (Throwable e) {
		logError(logName, "{} - Exception in 'syncHarmonySwitchesWithHubState' procedure {}", fileName, e.toString)
	}
]

rule "Harmony Starting Values"
    when
        System started
    then
        val List<GroupItem> harmonyGroups = newArrayList (g_Harmony)

        harmonyGroups.forEach[ hg | 
            val groupHub = hg.getMembers.filter[ h | h.type != "Switch" ].head
            syncHarmonySwitchesWithHubState.apply(logName, fileName, hg, groupHub.state.toString)
        ]
end

rule "Harmony Changed"
	when
        Member of g_Harmony received command
	then
		try {		
            logInfo(logName, "{} - {} received command: {}.", fileName, triggeringItem.name, receivedCommand)
            val List<GroupItem> harmonyGroups = newArrayList (g_Harmony)
            var GroupItem triggeringGroup
            var Boolean groupFound = false

            for (GroupItem harmonyGroup : harmonyGroups) {
                if (!groupFound)
                {
                    val triggerItemGroup = harmonyGroup.members.filter[ m | m.name == triggeringItem.name ]
                    if (triggerItemGroup !== null)
                    {
                        triggeringGroup = harmonyGroup
                        groupFound = true
                        logInfo(logName, "{} - found group associated with triggering item {}: {}.", fileName, triggeringItem.name, harmonyGroup.toString)
                    }
                }
            }

            var hubCommand = "PowerOff"

            if (triggeringItem.type == "Switch") //a switch item triggered the rule
            {
                if (receivedCommand == ON)
                {
                    hubCommand = "Watch " + triggeringItem.label
                }

                var associatedHub = triggeringGroup.members.filter[ h | h.type != "Switch"].head
                if (hubCommand != associatedHub.state) //hub state differs from command, so send to hub
                {
                    associatedHub.sendCommand(hubCommand)
                }
            }
            else //the hub triggered the rule
            {
                hubCommand = triggeringItem.state.toString
            }

            syncHarmonySwitchesWithHubState.apply(logName, fileName, triggeringGroup, hubCommand)
        } catch (Throwable e) {
			logError(logName, "{} - Exception in 'Harmony Changed' rule {}", fileName, e.toString)
		}
	end

It’s possible there may be bugs, but in my testing (with one hub), everything has worked the way I’d expect. Happy automating.

Thank @SuperPaul!
I will definitely give it a try :slight_smile:

Having said that, not sure if you’ve heard of it yet, but Apple just announced a few days ago that iOS 12 (coming fall of 2018) will add support for remotes…!!!
Now does that mean that we’ll immediately have support for Harmony remotes, probably not… but go figure :wink:

This is great! Im trying it right now. Im having some problems when changing the activity from the hub itself, it won’t update the switch…

I think that the received command rule is not triggering when launching an activity from the remote/hub

I changed the “received command” rule to “changed” and set the var received command = triggering item.state but I face the same problem as I used to face with my previous rule: when I changed between 2 activities (without powering off) it sends the off command and shuts everything off.

@iandronowicz, are you using the rules I published or the ones @SuperPaul did?
Can you share your rules file so we can try to help with it?

The @SuperPaul one! Had to add a changed rule to both of my harmony hubs since the received command one wouldn’t trigger. Also had to change the conditional where is searching in the harmony groups since the !== null was always returning true, even when the group wasn’t a match.

For now its working as it should:

import org.eclipse.xtext.xbase.lib.Procedures
import java.util.List

val logName = "rules"
val fileName = "harmony.rules"

val Procedures$Procedure4<String, String, GroupItem, String> syncHarmonySwitchesWithHubState = [ logName, fileName, triggerGroup, hubCommand |
    try {
        triggerGroup.members.filter[ hm | hm.type == "Switch"].forEach[ hm |
            var targetState = OFF
            if (hubCommand == "Watch " + hm.label) {
                targetState = ON
            }

            if (hm.state != targetState) {
                hm.postUpdate(targetState)
                logInfo(logName, "{} - Out of sync harmony switch item {} updated to {}}", fileName, hm.name, targetState)
            }
        ]
    } catch (Throwable e) {
                logError(logName, "{} - Exception in 'syncHarmonySwitchesWithHubState' procedure {}", fileName, e.toString)
    }
]

rule "Harmony Starting Values"
    when
        System started
    then
        val List<GroupItem> harmonyGroups = newArrayList (g_Harmony_Master, g_Harmony_Guest)

        harmonyGroups.forEach[ hg |
            val groupHub = hg.getMembers.filter[ h | h.type != "Switch" ].head
            syncHarmonySwitchesWithHubState.apply(logName, fileName, hg, groupHub.state.toString)
        ]
end

rule "Harmony Changed"
    when
        Member of g_Harmony_Master received command or Member of g_Harmony_Guest received command
    then
        try {
            logInfo(logName, "{} - {} received command: {}.", fileName, triggeringItem.name, receivedCommand)
            val List<GroupItem> harmonyGroups = newArrayList (g_Harmony_Master, g_Harmony_Guest)
            var GroupItem triggeringGroup
            var Boolean groupFound = false

            for (GroupItem harmonyGroup : harmonyGroups) {
                if (!groupFound) {
                    val triggerItemGroup = harmonyGroup.members.filter[ m | m.name == triggeringItem.name ]
                    if (triggerItemGroup.size() > 0) {
                        triggeringGroup = harmonyGroup
                        groupFound = true
                        logInfo(logName, "{} - found group associated with triggering item {}: {}.", fileName, triggeringItem.name, harmonyGroup.toString)
                    }
                }
            }

            var hubCommand = "PowerOff"

            if (triggeringItem.type == "Switch") {
                if (receivedCommand == ON) {
                    hubCommand = "Watch " + triggeringItem.label
                }

                var associatedHub = triggeringGroup.members.filter[ h | h.type != "Switch"].head
                if (hubCommand != associatedHub.state) {
                    associatedHub.sendCommand(hubCommand)
                }
            } else {
                hubCommand = triggeringItem.state.toString
            }
            syncHarmonySwitchesWithHubState.apply(logName, fileName, triggeringGroup, hubCommand)
        } catch (Throwable e) {
            logError(logName, "{} - Exception in 'Harmony Changed' rule {}", fileName, e.toString)
        }
    end

rule "Harmony Hub Changed"
    when
        Item Harmony_MasterBedroom_Activity changed or Item Harmony_GuestBedroom_Activity changed
    then
        try {
            var hubCommand = triggeringItem.state.toString
            logInfo(logName, "{} - {} received command: {}.", fileName, triggeringItem.name, hubCommand)
            val List<GroupItem> harmonyGroups = newArrayList (g_Harmony_Master, g_Harmony_Guest)
            var GroupItem triggeringGroup
            var Boolean groupFound = false

            for (GroupItem harmonyGroup : harmonyGroups) {
                if (!groupFound) {
                    val triggerItemGroup = harmonyGroup.members.filter[ m | m.name == triggeringItem.name ]
                    if (triggerItemGroup.size() > 0) {
                        triggeringGroup = harmonyGroup
                        groupFound = true
                        logInfo(logName, "{} - found group associated with triggering item {}: {}.", fileName, triggeringItem.name, harmonyGroup.toString)
                    }
                }
            }

            syncHarmonySwitchesWithHubState.apply(logName, fileName, triggeringGroup, hubCommand)
        } catch (Throwable e) {
            logError(logName, "{} - Exception in 'Harmony Changed' rule {}", fileName, e.toString)
        }
    end

Thank you for such an herculean solution. Totally agreed with the concept. It really worked for me.

Thank you overmuch. Work perfect for me :slight_smile: