[Alpha] Harmony hub instant status updates using node.js app


#1

I have something put together that uses a node.js app running on an internal server to listen for status updates from a Harmony hub on the same LAN. I'm not very good with javascript (go easy on me :sunglasses:), but seems to be working ok in my limited testing today. I'm putting this out there in hopes that someone more knowledgeable than me can take this and run with it to try and put together a comprehensive local integration with Hubitat.

Some notes:

  1. As of right now, this is not a control mechanism for Harmony...it listens for changes that were initiated in other ways and updates the corresponding switches in Hubitat.
  2. It uses harmonyhubjs-client from swissmanu's github page, I just wrote a small app to use the functionality from harmonyhubjs-client in the way I wanted.
  3. It does not require any Harmony credentials from the end user. harmonyhubjs-client uses some sort of guest authentication to open a websocket/xmpp connection from the node app to the Harmony hub. I'm hoping someone can dig in and find a way to use this same method to connect Hubitat directly to Harmony.
  4. It uses Hubitat Maker API to issue on/off commands to switches. These can be switches created by the ported Harmony Connect app from ST, virtual switches, physical switches, whatever.
  5. You will need to provide the node app your local Maker API endpoint, Harmony hub IP address (best to have it set as a static IP in your router), and a cross-reference between the activity IDs from Harmony and the corresponding switch you want to control in HE.
  6. If you have multiple Harmony hubs like I do, you will need a separate instance of the app running for each one.
  7. Switches are updated after the activity finishes running. I tried doing it before, but changing the Harmony Connect switch would trigger it to do a refresh, and since the activity wasn't done starting yet it was causing the switch to go off -> on -> off. This doesn't seem to happen when waiting until the end of the activity start up.

Download/extract harmonyhubjs-client from the link above, then run npm install inside the folder.
In my case, this is extracted to /home/pi/Harmony/harmonyhubjs-client folder.

Go up one level (in my case, to the /home/pi/Harmony folder) and create a new .js file. Call it whatever you want. You will need one for each hub you have. In my case, I have mbr.js (for master bedroom hub) and lr.js (for living room hub). Copy the code below and paste into your file.

	var harmony = require('./harmonyhubjs-client')
	var request = require('request')
	const makerLink = ''
	const activityXref = //create an array variable to associate activity IDs from Harmony to switch IDs from Hubitat
						[
							['',''], //Activity 1
							['',''], //Activity 2
							['',''], //Activity 3
							['',''] //Activity 4
						]
	const harmonyIP = '192.168.0.101'

	harmony(harmonyIP).then(function(harmonyClient) {
		console.log('hub client started for ' + harmonyIP)
		
		harmonyClient.getActivities()
			.then(function (activities) {
				console.log('activities found on hub')
				console.log('-----------------------')
				var i
				for(i = 0; i < activities.length; i++) {
					if(activities[i].label != 'PowerOff') {
						console.log(activities[i].id + ' (' + activities[i].label + ')')
					}
				}
				console.log('---------------------------')
				console.log('listening for state digests')
			})
			
		! function keepAlive(){
			harmonyClient.request('getCurrentActivity').timeout(5000).then(function(response) {
				setTimeout(keepAlive, 45000);
			}).catch(function(e){
				//disconnected from hub
			});
		}();
		
		harmonyClient.on('stateDigest', function(digest) {
			var statusCode = digest.activityStatus
			var currentlyOn = digest.runningActivityList
			var targetActivity = digest.activityId
			console.log('***received state digest***')
			if (statusCode == '0') { //Hub is off`
				if(currentlyOn == '' && targetActivity == '-1') {
					console.log('hub is off')
				} else {
					console.log('activity ' + currentlyOn + ' is now off')
					changeSwitch(currentlyOn, 'off')
				}
			}
			if (statusCode == '1') { //Activity is starting
				console.log('activity is starting')
			}
			if (statusCode == '2') { //Activity is started
				if(currentlyOn == targetActivity) {
					console.log('activity is started')
				} else {
					if(currentlyOn == '') {
						console.log('activity ' + targetActivity + ' is now on')
						changeSwitch(targetActivity, 'on')
					}
					else {
						console.log('activity ' + targetActivity + ' is now on')
						changeSwitch(targetActivity,'on')
						console.log('activity ' + currentlyOn + ' is now off')					
						changeSwitch(currentlyOn,'off')
					}
				}
			}
			if (statusCode == '3') { //Hub is turning off
				console.log('hub is turning off')
			}
		})
	}).catch(function(e){
		console.log('error')
	});



	function changeSwitch (activityId, changeTo) {
		var activityCount = activityXref.length
		var i
		var switchId = ""
		var tokenLabelPos = makerLink.indexOf('access_token')
		var makerURL = makerLink.substring(0,tokenLabelPos - 1)
		var makerToken = makerLink.replace(makerURL,'')
		for(i = 0; i < activityCount; i++) {
			if (activityXref[i][0] == activityId) {
				switchId = activityXref[i][1]
				console.log('xref activity ' + activityId + ' -> switch ' + switchId)
			}
		}
		var options = {
			uri: makerURL + "/" + switchId + "/" + changeTo + makerToken,
			method: 'GET',
			json: true
			}
		request(options, function(error, response, body) {
			if (!error && response.statusCode == 200) {
				console.log("sending " + changeTo + " command to switch " + switchId)
				//console.log(body)
			} 
		})
	}

Color Temp as trigger?
ST to HE migration plan questions
How to turn on zwave outlet with TV
#2

Update variables:
harmonyIP -> change to your hub's IP address
makerLink -> get the URL from the Get All Devices link in the Maker app and paste it here

activityXref array variable -> fill out with activity IDs from Harmony and switch IDs from hubitat. Add or remove array components as needed if you have more or less than the 4 slots that are in my example code. If you don't know your Harmony activity IDs, save the app with at least your IP address updated, and run it once and check the console output, it will tell you your activity names and IDs:
image

When you are finished, it should look like this:

Save the file, go to command prompt and run using "node mbr.js" or whatever you called your file, and you should see the same thing I posted from the console screenshot above. When activities change, you should see something like the following:

image


#3

One question I have for those of you who have messed around with this before (@ogiewon in particular) ....I had to use a keepAlive function (copied from one of the issues comments on swissmanu's page) to keep the connection open, otherwise it closes after 30 seconds or so. It sends a "get current activity" request every 10 seconds (which might be able to be extended). Is this functionally any different from what you were doing, and seeing negative effects/hub slowness from? I'm wondering if I'll see the same issue eventually.

**edit - I timed the disconnect and it's about a minute after the last activity. Changed the keepAlive interval on one of my hubs from 10 seconds to 45 seconds, hopefully that will work ok.


#4

For anyone who wants to pursue it, this file appears to contain the code the node app uses to get it's tokens to log in as a client to the Harmony hub. I think this is different than the now-disabled Logitech auth token URLs I've seen discussed in other threads.


#5

No issues so far with the 45 second keep alive. Updating the code in first post to reflect this change.


#7

@destructure00

Overall VERY cool. Instructions are a bit wonky but that is ok once you get through it. I would recommend creating a GitHub repository with your detailed instructions and code example. The 3-4 posts that are sorta out of sequence may cause most to fail.

For everyone attempting this you need to have Node.JS installed, a directory that has executable writes for running this as a service, your Harmony Hub(s) with static IPs AND leverage @ogiewon Harmony Hub HE app so the devices get populated and can be automated/controlled.

What I did differently is I installed the harmonyhubjs-client as a global node module instead of downloading and then installing:

Node.JS version 6 or lower do:
     sudo npm install -g harmonyhubjs-client --save

Node.JS version 7 or higher do (Node.JS does not permit modifying core install):
     su
     npm install harmonyhubjs-client --save

Because of this you need to change line 1 of the harmony.js script:

 var harmony = require('harmonyhubjs-client')

Finally after getting everything up and running I leveraged the npm forever-service to run my harmony.js as a service in the background even when rebooted and not logged into the Raspberry Pi:

   sudo npm install -g forever
   sudo npm install -g forever-service

   //Create sub directories for potential multiple harmony hubs (/share/harmony1)
   //Copy harmony.js to app.js (requirement of forever-service)
   cd /<directory path of harmony.js file>
   sudo forever-service install harmonyhub

This now works VERY good and I get instant access. Yes I know this is still not integrated directly into HE but I am using a Raspberry PI for VPN and also HomeBridge. So this was a very simple add-on to get instant notifications from Harmony. PLUS by following your directions you can have multiple harmony hubs in your home just by following the same instructions but have a Harmony2.js file.

Thank you for the great effort!


Access Hubitat through internet
#8

THANK YOU for your comments and suggestions on this. I work in SQL daily and have a decent amount of experience working in VBA but I have next to no experience in JS, just what I've been able to scrape together from digging through other people's code and some google searching. So it helps a ton to get input from others who actually know a thing or two about this stuff. I'll take a look at what I can do to incorporate your feedback when I get home later.

FWIW I am using the Harmony Connect app that was ported from ST for control, then using this thing to update the switches that HC created for my activities.


#9

So with this new “integration” I have been able to remove my reliance of a power meter on the TV to control lighting. I now just look for one of the harmony activities to turn on which is instant. Truly loving this.

@destructure00 are you needing assistance in getting the documentation together? I am curious if we could create a listening port for @ogiewon’s app so you could tie the two together programmatically. You would need the Maker API installed and the end-points configured but we should be able to do a local request for the xml file to sync up names for the activity ID and app id. Then auto populate the activities accordingly.


#10

Any assistance you can offer would be greatly appreciated :sunglasses:


#11

FYI I started having weirdness when switching activities, where the activity would switch then everything would shut off. I am thinking this is specific to using the ported HC app. Without digging through the HC code, it seems that issuing a command to any activity switch on the hub results in refreshing every switch on the hub. When changing activities, I had the node app issue both an on command to the new activity as well as an off command to the old activity. I'm not completely sure the sequence of events but somehow the off command to the old activity would end up turning the hub off. I commented out the off command when switching activities and for now it seems to have solved the problem. Turning on the new activity appears to cause the hub to refresh and set the old activity switch state correctly without explicitly telling it to turn off.


#12

I had this issue too but discovered I had the appID associated to the wrong activity. After fixing that all is good. Able to switch between activities without issues.


#13

I think my scenario might be specific to using the HC port.

I've also noticed that the connection to one or both hubs will terminate intermittently. Need to add something in there to detect if the connection closes and re-open it, rather than just stopping.

I'm intending to look into some of these things more, just haven't had the time yet.


#14

Alright I just got the same issue as you did switching between activities. Basically turned everything off. So now what I am thinking needs to happen is instead of changing the switch from on/off we just need to initiate a refresh. I have done the following in testing:

  1. Installed the NPM sleep module
    npm -g install sleep

  2. Changed all the code in on/off to refresh. I will look later at making this cleaner as currently it is now a mess.

  3. Added a (5) second sleep after activity

Code:

var sleep = require('sleep');
var harmony = require('harmonyhubjs-client')
	var request = require('request')
	const makerLink = 'http://HE-HUB-IP/apps/api/1069/devices?access_token=TOKENSTUFF'
	const activityXref = //create an array variable to associate activity IDs from Harmony to switch IDs from Hubitat
						[
							['34094364','856'], //Activity 1
							['34094397','857'], //Activity 2
							['34094408','855'], //Activity 3
						]
	const harmonyIP = 'HARMONY_HUB'

	harmony(harmonyIP).then(function(harmonyClient) {
		console.log('hub client started for ' + harmonyIP)
		
		harmonyClient.getActivities()
			.then(function (activities) {
				console.log('activities found on hub')
				console.log('-----------------------')
				var i
				for(i = 0; i < activities.length; i++) {
					if(activities[i].label != 'PowerOff') {
						console.log(activities[i].id + ' (' + activities[i].label + ')')
					}
				}
				console.log('---------------------------')
				console.log('listening for state digests')
			})
			
		! function keepAlive(){
			harmonyClient.request('getCurrentActivity').timeout(5000).then(function(response) {
				setTimeout(keepAlive, 45000);
			}).catch(function(e){
				//disconnected from hub
			});
		}();
		
		harmonyClient.on('stateDigest', function(digest) {
			var statusCode = digest.activityStatus
			var currentlyOn = digest.runningActivityList
			var targetActivity = digest.activityId
			console.log('***received state digest***')
			if (statusCode == '0') { //Hub is off`
				if(currentlyOn == '' && targetActivity == '-1') {
					console.log('hub is off')
				} else {
					console.log('activity ' + currentlyOn + ' is now off')
					changeSwitch(currentlyOn, 'refresh')
				}
			}
			if (statusCode == '1') { //Activity is starting
				console.log('activity is starting')
			}
			if (statusCode == '2') { //Activity is started
				if(currentlyOn == targetActivity) {
					console.log('activity is started')
				} else {
					if(currentlyOn == '') {
						console.log('activity ' + targetActivity + ' is now on')
						changeSwitch(targetActivity, 'refresh')
					}
					else {
						console.log('activity ' + targetActivity + ' is now on')
						changeSwitch(targetActivity,'refresh')
						console.log('activity ' + currentlyOn + ' is now off')					
						changeSwitch(currentlyOn,'refresh')
					}
				}
			}
			if (statusCode == '3') { //Hub is turning off
				console.log('hub is turning off')
			}
		})
	}).catch(function(e){
		console.log('error')
	});



	function changeSwitch (activityId, changeTo) {
		sleep.sleep(5)
		var activityCount = activityXref.length
		var i
		var switchId = ""
		var tokenLabelPos = makerLink.indexOf('access_token')
		var makerURL = makerLink.substring(0,tokenLabelPos - 1)
		var makerToken = makerLink.replace(makerURL,'')
		for(i = 0; i < activityCount; i++) {
			if (activityXref[i][0] == activityId) {
				switchId = activityXref[i][1]
				console.log('xref activity ' + activityId + ' -> switch ' + switchId)
			}
		}
		var options = {
			uri: makerURL + "/" + switchId + "/" + changeTo + makerToken,
			//uri: makerURL + "/" + switchId + "/Refresh/" + makerToken,
			method: 'GET',
			json: true
			}
		request(options, function(error, response, body) {
			if (!error && response.statusCode == 200) {
				console.log("sending " + changeTo + " command to switch " + switchId)
				//console.log(body)
			} 
		})
	}

This works really well as now I can switch between activities and not cause a loop of shutdowns. Give it a try. I will work on simplifying the code significantly today or tomorrow.


#15

Awesome! Thanks for your help on this.

I came home again today to both of my instances having terminated again. I've played around quite a bit with changing the keepAlive repeat time with no change in behavior. Now testing a change in the timeout time...instead of waiting 5 seconds for a response, I'm trying 15. Is there a way for the keepAlive function to call the outer function again if it senses a dropped connection? Is it harmony(harmonyIP) or harmonyClient() or something else?


#16

Great question. I do not have an answer to that. I am just a hack trying to assist. :woozy_face:


#17

Let me know if these code changes work for you. You are using the Logitech App Too in HE correct?


#18

I'm using the ported Logitech Connect app from SmartThings, the one you have to reuse the ST tokens on. I thought you were using @ogiewon's thing for control? Either way, I'd love to be able to get
both control and updates into a single app.

I'll try your code tomorrow, although mine has been working fine just sending the on commands and skipping the off. I also checked just now and one of the two hubs disconnected again even with the 15 second timeout, so I'm thinking it needs a way to reinitiate a connection when it's dropped. Tomorrow is a "work from home" day for me, hoping to be able to spend some time on this.


#19

I am using the Logitech connect app with tokens as well. The refresh works much better than the on/off and gives you immediate response. Not sure about the listener issue you are seeing. I am not seeing anything like that where the listener just drops. Also are you running this as a service using forever-service?


#20

I haven't switched it to a service yet, I just have an auto start file (can't remember the details, set that up a couple years ago) that opens a couple of lxterminal windows and runs different commands on startup. Does forever-service leave a window open where you can see console output?

I added a console log output to the catch section of the keepAlive function, so I know where to put something to restart the connection, just don't know how to do it yet.


#21

No output but you can access a log file to see what is going on. The benefit is it restarts the service if it stops for any reason.