[deprecated] UniFi Presence Sensor

@mike - I made some updates to the code for my purposes. Not sure if you want to use it yourself, but I did the following:

  • extracted the section of the main method that pulls data (after unifi login)
  • sleep 1 second, then calls said method recursively with no end
  • slapped together a cloud link to hubitat maker api. I could make this better if you want to use my code, but it works for my purposes.

If you don't want to use it, not a problem, but I figured I'd offer regardless. I was having an issue with scheduling a cron job in linux... not really my forte. Anyway, the result is you have a continually running script. Not necessarily great on memory in the long run, but I don't think it should be an issue; it's a pretty short script.

Good to hear that it's working for you.

Your new script will work well for arrivals, but not for departures. For as soon as you join the wifi network, then within 1 second, Hubitat will know. But for departures, Unifi hangs onto the connection for between 5 and 10 minutes before dropping it, meaning you cant rely on it for locks, doors etc.

If you can figure that out - the Unifi side, I'd be impressed!

Yeah, you're right. Right after I sent that, I realized it wasn't working XD I think I have a solution... I'll test it over the next couple of days and update you here.

This looks awesome!

So bad news, my ideas didn't work... I tried to get it to log out every 5 minutes, then log back in, but I still only got it to last about 15-20 minutes at best before failing out. I did, However, make the following "enhancements"

#!/usr/bin/python
# Config File Location
#	default is /etc/unifipresence.conf
confFile = "./unifipresence.conf"

#Imports
import json
import requests, requests.utils
import sys, traceback
import cookielib
import datetime

from ConfigParser import SafeConfigParser
from requests.packages.urllib3.exceptions import InsecureRequestWarning

config = SafeConfigParser()
config.read(confFile)

HubitatIP = config.get('DEFAULT', 'HubitatIP').strip("'\" \t")
CloudAuth = config.get('DEFAULT', 'CloudAuth').strip("'\" \t")
MakerAPI = config.get('DEFAULT', 'MakerAPI').strip("'\" \t")
AccessToken = config.get('DEFAULT', 'AccessToken').strip("'\" \t")
urlUniFiBase = config.get('DEFAULT', 'urlUniFiBase').strip("'\" \t")
unUniFi = config.get('DEFAULT', 'unUniFi').strip("'\" \t")
pwUniFi = config.get('DEFAULT', 'pwUniFi').strip("'\" \t")
siteUniFi = config.get('DEFAULT', 'siteUniFi').strip("'\" \t")
cookieFile = config.get('DEFAULT', 'cookieFile').strip("'\" \t")
tmpFile = config.get('DEFAULT', 'tmpFile').strip("'\" \t")

#Setup
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

def main():
	sesUniFi = requests.Session()
	sesUniFi.verify = False
		
	jar = cookielib.MozillaCookieJar()
	try:
		jar.load(cookieFile, ignore_discard=True, ignore_expires=True)
		sesUniFi.cookies = jar
	except:
		
		loginUniFi(sesUniFi)
		pass # No cookie file, so login first

	try:
		url = urlUniFiBase + '/api/s/' + siteUniFi + '/stat/report/hourly.site'
		r = sesUniFi.get(url)
	except:
		sys.exit("UniFi Connection Test Failed")	

	if not r.status_code == requests.codes.ok:
		loginUniFi(sesUniFi)
	jar.save(cookieFile, ignore_discard=True, ignore_expires=True)
	
	try:
		url = urlUniFiBase + '/api/s/' + siteUniFi + '/stat/sta'
		r = sesUniFi.get(url)
	except:
		sys.exit("UniFi Client Listing Failed")
		
	clientsUniFi = r.json()
	# print clientsUniFi
	# for clientUniFi in clientsUniFi['data']:
#            print clientUniFi['mac']
	
	# Read/Create temp status file
	# 	clientStatus = open(tmpFile,'w+')
	# Loop through config 
	for clientConfig in config.sections():
		# print '\nSection:', clientConfig
		if not (config.has_option(clientConfig, 'DeviceID')):
			print(datetime.datetime.now(), clientConfig, "must contain DeviceID")
		if not (config.has_option(clientConfig, 'macAddr')):
			print(datetime.datetime.now(), clientConfig, "must contain macAddr")
		else:
			DeviceID = config.get(clientConfig, 'DeviceID').strip("'\" \t")
			macAddr = config.get(clientConfig, 'macAddr').strip("'\" \t")
			lastStatus = getLastStatus(DeviceID)
			# print 'Client %s\nDeviceID %s\nLast Status %s\nMacAddr %s' % (clientConfig, DeviceID, lastStatus, macAddr)
			# search clientsUniFi for macAddr
			currStatus = macSearch(macAddr, clientsUniFi)
			# print(datetime.datetime.now()), "Current Status", currStatus
			setLastStatus(DeviceID, currStatus)
			if not ( lastStatus and currStatus == lastStatus):
				print(datetime.datetime.now(), 'Update Hubitat for ' + clientConfig + ' with status ' + currStatus)
				updateHubitat(HubitatIP, CloudAuth, DeviceID, currStatus)

#Defs
def getLastStatus(DeviceID):
	try:
		with open(tmpFile, "r") as jsonFile:
			data = json.load(jsonFile)
		return data[DeviceID]
	except:
		return False

def setLastStatus(DeviceID, status):
	try:
		with open(tmpFile, "r") as jsonFile:
			data = json.load(jsonFile)
			data[DeviceID] = status
	except:
		data = {}
		data[DeviceID] = status
	with open(tmpFile, "w") as jsonFile:
		json.dump(data, jsonFile)
	
def loginUniFi(sesUniFi):
	url = urlUniFiBase + "/api/login"
	auth = {'username': unUniFi, 'password': pwUniFi}
	try:
		r = sesUniFi.post(url, data=json.dumps(auth))
	except requests.exceptions.RequestException as e:
		print(e)
		sys.exit("UniFi login failed")
	if not r.status_code == requests.codes.ok:
		print(datetime.datetime.now()), "Login failed with status code ", r.status_code
		sys.exit(1)
	else:
		print(datetime.datetime.now()), "UniFi logged in"

def macSearch(macAddr, clientsUniFi):
	for clientUniFi in clientsUniFi['data']:
		if clientUniFi['mac'].upper() == macAddr.upper():
			return 'Present'
	return 'Absent'

def updateHubitat(HubitatIP, CloudAuth, DeviceID, currStatus):
	if currStatus == 'Present':
		status = 'on'
	else:
		status = 'off'
	if CloudAuth == "":
		url = HubitatIP + "/apps/api/" + MakerAPI + "/devices/" + DeviceID + "/" + status + "?access_token=" + AccessToken
	else:
		url = HubitatIP + "/api/" + CloudAuth + "/apps/" + MakerAPI + "/devices/" + DeviceID + "/" + status + "/?access_token=" + AccessToken
	# print url
	try:
		r = requests.get(url)
	except requests.exceptions.RequestException as e:
		print(e)
		sys.exit("Hubitat Update Failed")
	if not r.status_code == requests.codes.ok:
		print(datetime.datetime.now()), "Hubitat update failed with status code ", r.status_code
		if int(r.status_code) == '500':
			print(datetime.datetime.now()), "Is this really a Virutal Switch?"
	# else:
		# print(datetime.datetime.now()), "hubitat Updated"
	
if __name__ == '__main__':
	main()


# Configuration

[DEFAULT]

#Hubitat details

HubitatIP = "https://cloud.hubitat.com"

CloudAuth = "cloud string" # first long string in MakerApi area. Leave blank within quotes if local

MakerAPI = '123'

AccessToken= 'access token string'

#UniFi Manager Base URL

urlUniFiBase = 'https://localhost:8443'

unUniFi = 'username' #create a read user for unifi

pwUniFi = 'unifiPass'

siteUniFi = 'default'

#Files

#location of cookie file, tempfile

cookieFile = 'autofile.unifipresence.cookie'

tmpFile = 'autofile.unifipresence.status'

#Client Information

# User1

[U1]

macAddr = '00:00:00:00:00:00'

DeviceID = '100'

# User2

[U2]

macAddr = '00:00:00:00:00:00'

DeviceID = '101'

Hopefully this will be useful for someone. I could also implement a Unifi cloud URL if someone is interested in that... I just didn't need it, so I didn't use it.

I'm fairly confident this works, actually... Add a file to the folder. You can name the file whatever you like, but I called it runthis.py. NOTE: this may not work for Windows users. According to the documentation, os.system() only works for Unix (linux) and Mac systems. If you test it on Windows, let me know. Another note, I tried to use recursion... apparently there is a limit to how much you can use recursion in Python :man_shrugging: @mike I sent you a pull request on GitHub.

import time
import os

while True:
    os.system('python unifipresence.py')
    time.sleep(10)
1 Like

Ive been toying with this problem for a while now.

Ive two houses and one runs Domoticz and for that I have a LUA script that logins into my apple account every 5 minutes, retrieves the location and then does some fancy stuff around working out the distance from the house using Google API's. If within a certain distance then it updates Domoticz with the location and sets a home switch. It actually works really well. The drawback is that you have to know the apple ID login details. A side benefit of this is that i have log files going back years, with all my geolocation data. One day I may map that! See the following link for details:
https://www.domoticz.com/forum/viewtopic.php?t=13318

The other way i do this is via Life360, it works okay too. And there is a native Hubitat app for it. The drawback is that our young adults wont install this on there phones..

The third way im experimenting with is to use the Unifi, as mentioned earlier. I've a NodeRed Unifi plugin and it then logs the data currently only to InfluxDB. As you can see from the graph (for an Android phone, Apple phones are much better at being connected) there are big spikes in wifi connectivity that i would need to work around. Im trying to think about how to do a rolling window average..

1 Like

Have you considered using wireless probes? Seems like there are lots of hoops being jumped through, so maybe this (in some modified form) would work GitHub - 0xOperant/wuds: Wifi User Detection System

Just an idea.

Does anyone have a copy of Cobra's device driver? It looks like his github page is blank now...

He will be back soon. His webpage says a few weeks. With a new partner as well. Looks very promising :+1:

1 Like

I ended up going a different way while we wait on Cobra's new site... If you create "Virtual Presence" devices instead of switches things actually work like I would have expected. You just have to update the python to send "arrived" and "departed" instead of on and off. I haven't been running things long but it seems to be working for a day or so. :slight_smile:

1 Like

Thanks for the thought, I've added WUDS GitHub - 0xOperant/wuds: Wifi User Detection System to my project list. Now if only more hours in the day!

1 Like

Hey all

I've only recently got a hubitat and am in the process of migrating my exiting automation's (where possible).

My current automation's are all currently run from a python api i've written, which forms the core of my house. One of the end points on this api enables my CCTV motion sensors when there is no one left in the house and disables them on return. This is triggered from the leaving and joining on mine and my wifes phones on the Unifi access point. The wifi signal is so strong the phones join from up the street and disable the cameras before we trip them.

I'm using an event listener for this, as you can subscribe to events on the unifi controller.

I have a simple node express service that listens for connection/disconnection events and if our phone mac addresses are in the message call the api and do arm/disarm.

I fire these different services up from a mono repo using docker/docker compose.

As someone mentioned above i does take about 5-10 minutes for the disconnection event to occur.

Happy to share some code if this would be of interest to anyone

I’d love to see that code. I recently got a UniFi Dream Machine and looking for ways to integrate its events into Hubitat

Sorry for the delay.

Here's a link to a gist: unifi controller event listener · GitHub

There are some placeholders in the node app

For the device you want to monitor (this code currently monitors two, mine and my wifes phones)

These three speak for themselves.

is the IP of my home grown api, but this could be replaced with a hubitat end point

Here's details on the library being used in the node app https://www.npmjs.com/package/unifi-events

This is exactly what I’d like to do. I noticed in another thread you are enabling/disable reolink cameras, so I’ve got that bit sorted. I’m still not clear though, are you still running code on the Pi, or can it all be done from Unifi hardware and hubitat rules?

Best to use this release, not mine. Different @mike10

Thanks, I’ll give that a go. :+1: