Google Home Notifier

Unless I’m misunderstanding you, that’s exactly what I’m doing (except I’m sending it over my local intranet). Here’s my piston:

Can you post a picture of your piston for comparison?

Is the URL you’re using a local URL? It looks like it is to me. I’m not sure if smartthings is running this piston locally or not. If it’s not locally then it won’t work. I will try my local IP later, when everyone’s awake, to see if that works but I think that may be your issue.

Yes, I was using a URL with a local IP address, but I know the POST request was reaching my server because I saw it in Wireshark.

But, I modified CoRE to send it to my public IP and routed the port to my server and… tada… it worked.

I guess CoRE is formatting the request differently, depending on whether or not it’s a public IP address.

Is this a bug @ady624?

Yep I just set up ngrok and made a request to the ngrok address and it worked…

It looks like internal and external requests are handled differently in the CoRE code. I’m not 100% sure on this (not a dev) but it looks like internal requests are sent by the hub (HubAction) and external requests are processed/sent though the cloud. I don’t see any code that builds a body for an internal request, which is necessary for the POST method.

def internal = uri.startsWith("10.") || uri.startsWith("192.168.")
	if ((!internal) && uri.startsWith("172.")) {
		//check for the 172.16.x.x/12 class
		def b = uri.substring(4,2)
		if (b.isInteger()) {
			b = b.toInteger()
			internal = (b >= 16) && (b <= 31)
		}
	}
	def data = [:]
	for(variable in variables) {
		data[variable] = getVariable(variable)
	}
	if (internal) {
		try {
			debug "Sending internal web request to: $uri", null, "info"
			sendHubCommand(new physicalgraph.device.HubAction(
				method: method,
				path: (uri.indexOf("/") > 0) ? uri.substring(uri.indexOf("/")) : "",
				headers: [
					HOST: (uri.indexOf("/") > 0) ? uri.substring(0, uri.indexOf("/")) : uri,
				],
				query: data ?: null
			))
		} catch (all) {
			debug "Error executing internal web request: $all", null, "error"
		}
	} else {
		try {
			debug "Sending external web request to: $uri", null, "info"
			def requestParams = [
				uri:  "${protocol}://${uri}",
				query: method == "GET" ? data : null,
				requestContentType: (method != "GET") && (contentType == "JSON") ? "application/json" : "application/x-www-form-urlencoded",
				body: method != "GET" ? data : null
			]
		def func = ""
		switch(method) {
			case "GET":
				func = "httpGet"
				break
			case "POST":
				func = "httpPost"
				break
			case "PUT":
				func = "httpPut"
				break
			case "DELETE":
				func = "httpDelete"
				break
			case "HEAD":
				func = "httpHead"
				break
		}
		if (func) {
			"$func"(requestParams) { response ->
				setVariable("\$httpStatusCode", response.status, true)
				setVariable("\$httpStatusOk", response.status == 200, true)
				if (importData && (response.status == 200) && response.data) {
					try {
						def jsonData = response.data instanceof Map ? response.data : new groovy.json.JsonSlurper().parseText(response.data)
						importVariables(jsonData, importPrefix)
					} catch (all) {
						debug "Error parsing JSON response for web request: $all", null, "error"
					}
				}
			}
		}
	} catch (all) {
		debug "Error executing external web request: $all", null, "error"
	}
}
1 Like

I got it to work using the internal IP by modifying this section of the CoRE code. The if (internal) part starts at line 7620. The current published CoRE code always sends variables as a query appended to the end of the URL regardless of the method, and does not send a request body, which is where the variables should be sent in a POST request. I added the line so the content type is defined in the request header, added a section for Body, and made the query dependent on the method. Most of this is just copypasta from the external request section.

if (internal) {
	try {
		debug "Sending internal web request to: $uri", null, "info"
		sendHubCommand(new physicalgraph.device.HubAction(
			method: method,
			path: (uri.indexOf("/") > 0) ? uri.substring(uri.indexOf("/")) : "",
			headers: [
				HOST: (uri.indexOf("/") > 0) ? uri.substring(0, uri.indexOf("/")) : uri,
                "Content-Type": (method != "GET") && (contentType == "JSON") ? "application/json" : "application/x-www-form-urlencoded" //added by chris
			],
			//query: data ?: null,
            query: method == "GET" ? data : null, //added by chris
			body: method != "GET" ? data : null //added by chris

		))
	} catch (all) {
		debug "Error executing internal web request: $all", null, "error"
	}
}
2 Likes

And you say you’re not a dev :wink: Good job!

Now we just need to get @ady624 to put it into his repository.

2 Likes

I can hack through someone else’s code and make modifications, but I’d be lost if I had to start from scratch and build my own stuff :fearful:

Speaking of hacking through code, I modified the example.js and google-home-notifier.js files to allow passing a target GH IP address variable through the web request. The way the code was originally set up, you had to hard code a GH device. I have two problems with this:

  1. When the mdns browser polls my network to find all google cast devices, it doesn’t always return a full list, and if my target GH is not included in the list, example.js errors out when I send commands
  2. I have 3 GH’s, and I want to be able to target specific ones without having multiple google-home-notifier installations

Here’s the text of my modified example.js file:

var express = require('express');
var googlehome = require('./google-home-notifier');
var bodyParser = require('body-parser');
var app = express();
const serverPort = 8080;

var urlencodedParser = bodyParser.urlencoded({ extended: false });

app.post('/google-home-notifier', urlencodedParser, function (req, res) {
  if (!req.body) return res.sendStatus(400)
  console.log(req.body);
  var text = req.body.text;
  var ipaddress = req.body.ipaddress
  if (text && ipaddress){
    res.send(ipaddress + ' will say: ' + text + '\n');
    googlehome.notify(text, ipaddress, function(res) {
      console.log(res + " " + ipaddress);
    });
  }else{
    res.send('Please POST "text=Hello Google Home"');
  }

})

app.listen(serverPort, function () {
    console.log('Make a POST request to this device');
    console.log('example:');
    console.log('curl -X POST -d "text=Hello Google Home" -d "ipaddress=192.168.0.119" http://192.168.0.110:8080/google-home-notifier');
})

Here’s the text of my modified google-home-notifier.js file:

var Client = require('castv2-client').Client;
var DefaultMediaReceiver = require('castv2-client').DefaultMediaReceiver;
//var mdns = require('mdns');
var googletts = require('google-tts-api');
//var browser = mdns.createBrowser(mdns.tcp('googlecast'));
var device = function(name) {
    device = name;
    return this;
}


var notify = function(message, deviceIP, callback) {
    console.log('Address = ' + deviceIP + ', message = ' + message);
    getSpeechUrl(message, deviceIP, function(res) {
      callback(res);
    });
}

var getSpeechUrl = function(text, host, callback) {
  googletts(text, 'en', 1).then(function (url) {
    onDeviceUp(host, url, function(res){
      callback(res)
    });
  }).catch(function (err) {
    console.error(err.stack);
  });
}

var onDeviceUp = function(host, url, callback) {
  var client = new Client();
  client.connect(host, function() {
    client.launch(DefaultMediaReceiver, function(err, player) {
      var media = {
        contentId: url,
        contentType: 'audio/mp3',
        streamType: 'BUFFERED', // or LIVE
      };
      player.load(media, { autoplay: true }, function(err, status) {
        client.close();
        callback('Device notified');
      });
    });
  });

  client.on('error', function(err) {
    console.log('Error: %s', err.message);
    client.close();
    callback('error');
  });
}

exports.device = device;
exports.notify = notify;

Piston is the same, I created a second local variable called ipaddress and send both text and ipaddress variables along with the request

Output from example.js

1 Like

Looks good. I’ll give this a try when I get some time. One thing I noticed with the existing google-home-notifier code is that after the first message is received, it starts tying up 100% of one of the CPU cores.

Do you see the same thing?

I’m not seeing that. I just killed and re-launched example.js and sent it a command, node jumped up to 18% CPU for a brief moment and then dropped out of the list

Well, with your new code, it’s not happening for me either. So, good job once again!

1 Like

Hi all - wondering if anyone knows how to find the IP Address of a Google Home Group? Would like to send my notifications to a group that contains all three of my Homes…

P.S. - thanks for the IP Address version @destructure00!! Worked like a charm for me! :smiley:

The original application called a mdns application to scan the local network for GH devices and get their IP addresses. Since my modified version is passed an IP address, there’s no need to do the network scan to look it up, so I took that piece out of the app. Maybe the mdns lookup was the piece that was causing the CPU to get tied up.

From what I’ve seen (and I could be very wrong, this is just based on the output I posted below), a cast group doesn’t have it’s own IP address…rather, it “lives” on a different port of one of your GH devices. So where you might post a notification to a specific device at 192.168.0.112:8009, you would use 192.168.0.112:43009 to hit the group. I haven’t found in the google-home-notifier app where the ports are specified. I’ll do some digging and see what I can find, I’d like to be able to hit the group too instead of a single device.

1 Like

Some food for thought - I just noticed that if music is currently playing on the Home, the notifier stops the music and it doesn’t resume after. Makes sense considering the notifier is a new cast session, but wanted to point it out. I’m not sure if I really want my device(s) to announce when someone gets home or whatever else if it means my music is cut off?

I’ve been thinking about the suspend / resume as I’ve been working on a separate implementation of chromecasts / google homes as smartthings devices

I don’t think there’s any native capability of the chromecasts to resume / restore an app, so potential solutions are:

  • delay the notification until the current app is idle (that may be okay for some kinds of notifications, but not so much for others)
  • If the current app is an app that can play an audio file (DefaultMediaReceiver for sure, but probably not many others), insert the notification at the beginning of the queue
  • Look into saving current application state and restoring it afterwards (that might be possible for some where the protocol is known, like Youtube).
2 Likes

It’s right in the example.js which initializes the server.

const serverPort = 8080;

That’s the port the server listens on. I’m talking about the port to send the GH requests to. Two different things.

I was just thinking earlier about a DTH for GH. I’ll check this out tomorrow when I’m more awake and more sober :wink:

You’re right - I misunderstood.

I had the same experience with the old code using 100% cpu. @destructure00 code looks to have fixed that for me as well. Good work.