UDP mDNS Discovery assistance


#1

@patrick or @chuck.schwer thoughts?

Hello all,

So I found this code to see if I can use it as an example to help educate me on mDNS discovery. I have adapted the code to work in Hubitat BUT I have no idea why this will not discover any devices on the network. My eventual goal in this learning is to query a single known IP address for mDNS results. Does HubAction not support this type of querying of the network? Or am I just off on my coding below? Any and ALL help is appreciated. :slight_smile:

definition(
    name:"mDNS Discovery",
    namespace: "Aaron Ward",
    author: "Aaron Ward",
    description: "mDNS",
    category: "tool",
    iconUrl: "",
    iconX2Url: "",
    iconX3Url: ""
    )

preferences {
	section("Title") {
		page(name:"firstPage", title:"mDNS Device Setup", content:"firstPage")
	}
}

def installed() {
//	log.debug "Installed with settings: ${settings}"

	initialize()
}

def updated() {
//	log.debug "Updated with settings: ${settings}"

	unsubscribe()
	initialize()
}

def initialize() {
	// TODO: subscribe to attributes, devices, locations, etc.
}

def discover() {
	sendHubCommand(new hubitat.device.HubAction("lan discover mdns/dns-sd .hubitat._tcp._site", hubitat.device.Protocol.LAN))
}

def firstPage()
{
		int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
		state.refreshCount = refreshCount + 1
		def refreshInterval = 5

		//log.debug "REFRESH COUNT :: ${refreshCount}"

		if(!state.subscribe) {
			subscribe(location, null, locationHandler, [filterEvents:false])
			state.subscribe = true
		}

		if((refreshCount % 3) == 0) {
			discover()
		}

		def devicesDiscovered = devicesDiscovered()

		return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
			section("Please wait while we discover your device. Select your device below once discovered.") {
				input "selectedDevices", "enum", required:false, title:"Select Devices \n(${devicesDiscovered.size() ?: 0} found)", multiple:true, options:devicesDiscovered
			}
		}
}

def devicesDiscovered() {
	def devices = getDevices()
	def map = [:]
	devices.each {
		def value = it.value.name ?: "mDNS Device: ${it.value.mac}"
		def key = it.value.ip + ":" + it.value.port
		map["${key}"] = value
	}
	map
}

def getDevices()
{
	if (!state.devices) { state.devices = [:] }
	state.devices
}

def locationHandler(evt) {
	def description = evt.description
	def hub = evt?.hubId

	def parsedEvent = parseEventMessage(description)
	parsedEvent << ["hub":hub]

	if (parsedEvent?.mdnsPath)
	{
		def devices = getDevices()

		if (!(devices."${parsedEvent?.mac?.toString()}"))
		{ //device does not exist
			devices << ["${parsedEvent.mac.toString()}":parsedEvent]
		}
		else
		{ // update the values

			log.debug "Device was already found in state..."
			def d = device."${parsedEvent.mac.toString()}"
			boolean deviceChangedValues = false

			if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
				d.ip = parsedEvent.ip
				d.port = parsedEvent.port
				deviceChangedValues = true
				log.debug "mdns device's port or ip changed..."
			}

			if (deviceChangedValues) {
				def children = getChildDevices()
				children.each {
					if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
						log.debug "updating dni for device ${it} with mac ${parsedEvent.mac}"
						it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port))
					}
				}
			}
		}
	}
}

private def parseEventMessage(String description) {
	def event = [:]
	def parts = description.split(',')
	parts.each { part ->
		part = part.trim()
		if (part.startsWith('devicetype:')) {
			def valueString = part.split(":")[1].trim()
			event.devicetype = valueString
		}
		else if (part.startsWith('mac:')) {
			def valueString = part.split(":")[1].trim()
			if (valueString) {
				event.mac = valueString
			}
		}
		else if (part.startsWith('networkAddress:')) {
			def valueString = part.split(":")[1].trim()
			if (valueString) {
				event.ip = valueString
			}
		}
		else if (part.startsWith('deviceAddress:')) {
			def valueString = part.split(":")[1].trim()
			if (valueString) {
				event.port = valueString
			}
		}
		else if (part.startsWith('mdnsPath:')) {
			def valueString = part.split(":")[1].trim()
			if (valueString) {
				event.mdnsPath = valueString
			}
		}
		else if (part.startsWith('headers')) {
			part -= "headers:"
			def valueString = part.trim()
			if (valueString) {
				event.headers = valueString
			}
		}
		else if (part.startsWith('body')) {
			part -= "body:"
			def valueString = part.trim()
			if (valueString) {
				event.body = valueString
			}
		}
	}

	event
}

Event log errors:
[app:2314](http://10.0.2.38/logs#app2314)2019-04-03 08:24:35.930 pm [warn](http://10.0.2.38/installedapp/configure/2314)Cannot get property 'host' on null object


#2

We don't support mdns discovery at this time.


#3

So is there any undocumented way of doing this in HE?


#4

You can use upnp for lan discovery which is kind of the same thing, right? Here's an example.

Check the app for how it discovers devices.


#5

Unfortunately no. I am trying to build a better iPhone mouse trap and the only port iPhones listen to when in deep sleep is MDNS.


#6

Are you looking to do it for lan presence?


#7

Heh... okay. You lost me at iPhone mouse trap. If it's a trap just plug it in and change the settings to stay awake. Or can you not do that with an iPhone? Why is an iPhone involved in a trap at all? Shouldn't you be looking at... ESP8266s or RPIs or something?

I have never looked at one of my old phones and said to myself... "You know what? I'm going to build a mousetrap out of that." :stuck_out_tongue:


#8

I am attempting to build a better WiFi presence detection. The current HTTPGet request or uPnP solutions do not work reliably because the iPhone goes into sleep mode and turns off 99% of its wireless stack for listening/responding to requests. All iPhones/iPads listen for MDNS even in deep sleep. I have proven this true doing a netcat call on a RPi. I already have a working third party solution for this but would rather have a native solution in HE. UDP calls from HE were promising until I discovered that mDNS is not a valid HubAction call. Hence my statement of building a better mouse trap. Btw this is not an issue for android users.


#9

Ya. I’ve been playing with this stuff for a year. The only native solution I’ve see with HE was the http call and that’s only because the error returned was different. But the iPhone deep sleep is a pain as it will go to sleep up to 15 minutes at a time. I had scripts running to monitor this across two phones for months just to find the right time.

Right now though I have a script running on my pfsense box that uses arping to determine the lan presence. It works amazing. 90% of the time the phone doesn’t go offline for more than 10s and it’s always respond back within a minute. So I set it up to set as offline after 2 minutes (using maker API) and set up a notification to let me know when it goes offline and comes online. In the last two months I had 1 false positive. I couple to with geofencing and have had 100% success with using the phone as presence.

I know it’s not native but it’s been way better than ping or http calls.


#10

This is excellent news. Do you mind sharing your code? My goal now is to create a similar app as Hubitat with 2way communication. Was thinking of a node.js service querying for apple and android devices, providing those results in an app that then can create Presence devices that will be also updated by the node.js service. Anything to jumpstart my expedition would be appreciated. :grin:


#11

There are also scripts around that run on *WRT/tomato routers. I don't know if those have a mDNS library available or not. There is even something for Unifi floating too I think. Chances are that not all of those solutions where developed by Android users and there is something floating around for your iPhone presence as well.

Just a couple of other options to hunt in case one fits better.