Ubiquiti UniFi - Plugin Potential?

The wifi connection to the devices I had were very flaky … so I put the project on the side for now.
I really wanted them for outside … where I need more range … but I already have Wifi coverage.

Richard, would it make sense to share the code you may have written for others to try to develop the plugin or is it something that you would like to keep for yourself and eventually release sometime in the future?

It’s not in a very workable state right now … It was just some prototype code to verify I could talk to it. But I spent all the time trying to make the connection/configuration easy and reliable.

I came across an interesting post on the Ubiquiti forum, which has a shell script looking at occupancy and the triggering an action via a http call to Vera.

https://community.ubnt.com/t5/UniFi-Wireless/Shell-Script-for-occupancy/td-p/1507765

The resulting discussion may be of use/interest to help with a Unifi presence detection plugin/app

Hi @RTS

Did you make any more progress with your Ubiquiti plugin ?

[quote=“parkerc, post:25, topic:192116”]Hi @RTS

Did you make any more progress with your Ubiquiti plugin ?[/quote]

Or would you be prepared to share the code? I’m thinking of modifying the Ping Sensor Plugin - basically replace the section of code that runs the ping

local returnCode = os.execute("ping -c 1 " .. address)

With something that uses the Unifi API to ask if an IP Address is present on the WLAN. Rationale for IP rather than MAC is minimising the amount of change required to the Ping Sensor code.

I’d be happy to hardcode all of the config for my Unifi controller.

I had a lot of problems with the Ubiquiti devices … discovery and configuration was not very reliable … I did not want a continuous support project … so I dropped it.

@parkerc

I have created a working “Unifi Ping” prototype, based on the existing Ping Sensor that uses the Unifi controller and will look up MAC, IP or Hostname (or pretty much anything that shows up in the controller status).

My only concern is the execution time to grab the contents from the Unifi server. Gimme a little time to test it works as expected and I’ll share.

General question (RTS?): is there a recommended location on Vera (via SSH) to store user created shell scripts? I’m using /tmp at the moment. Ideally someone can convert the .sh script to lua and embed it in the plugin, but that’s beyond my capability at the moment.

Ok… latest update.

My proof of concept hacked version has been working well for 2 days now monitoring 2 iphones and 2 androids. It’s been perfect, much more reliable than ping was.

Today I ‘repackaged’ the plugin, ditching UI5 legacy and replacing anything ‘ping’ with ‘unifi’ and created some snazzy UAP icons for status indication as well as UI messages when it’s doing a unifi query.

Now I’m going to start on some enhance/extend, fix the UI and move any hardcoded script stuff into UI variables. At that point, I’ll post it for people to try.

First release of plugin can be found here:

http://forum.micasaverde.com/index.php/topic,50187.msg326848.html#msg326848

It’s amazing the stuff I find myself coming back to in my ever continuing Vera / Lua journey:-)

I never did find a right way to integrate my UniFi home network set up with Vera, so I thought I’d open this thread up again and focus on this Unifi Presence Lua script/code (i’m never quite sure what to call it) to connect to my UniFi Controller and retrieve a .json and then search for the specified device information.

I’ve been looking to update this code to generate a customer report, and have tried a few variations. The main part of the code is as follows, which returns a large .json file

function UnifiController()
	unifi_username = "user" -- Username for example admin
	unifi_password = "pass" -- Password for example 1234546
	unifi_controller_ip = "192.168.1.207" -- Controller IP
	unifi_controller_port = "8443"  -- Controller port, default is 8443
	cookie = "/tmp/unifi_cookie_lua" -- Temp cookie file
	tempfilename = "/tmp/UnifiController.tmp" -- Temp JSON output file

	local f = io.popen("stat -c %Y " .. tempfilename)
	local last_modified = f:read()
	if (os.difftime (os.time(), last_modified) > 30) 
	then
		-- URL for logging in, query and logging out
		url_login = 'curl --cookie ' .. cookie .. ' --cookie-jar ' .. cookie .. ' --insecure -H \'Content-Type: application/json\' -X POST -d \'{"password":"' .. unifi_password .. '","username":"' .. unifi_username .. '"}\' https://' .. unifi_controller_ip .. ':' .. unifi_controller_port .. '/api/login'
		url_open = 'curl --cookie ' .. cookie .. ' --cookie-jar ' .. cookie .. ' --insecure -s -o '..tempfilename..' --data "json={}" https://' .. unifi_controller_ip .. ':' .. unifi_controller_port .. '/api/s/default/stat/sta'
		url_logout = 'curl --cookie ' .. cookie .. ' --cookie-jar ' .. cookie .. ' --insecure https://' .. unifi_controller_ip .. ':' .. unifi_controller_port .. '/logout'
		-- Execute url
		-- Execute url
		read_login = os.execute(url_login)
		read_open = os.execute(url_open)
		read_logout = os.execute(url_logout)
	end

	file = io.open(tempfilename, "r")
	while true do 
		line = file:read("*line")
			if not line then 
				break 
			end
                return line
	end
	file:close()
end

print(UnifiController())

I have validated the returned json online and it’s confirmed as good, and now I want to work with it, but I’m not sure of the best way to do it.

I either time-out or get an error on line 355 - example below.

local json = require "dkjson"
local unifidata = json.decode(UnifiController())
for k, v in pairs(unifidata) do
			print(k,v)
		end

Returns

Runtime error: Line 355: bad argument #1 to 'strfind' (string expected, got nil)

Here is the .json design (when I run it through a json designer app)

The UnifiController() function has only two code paths to exit:

  1. The return statement inside the while loop near the bottom.
  2. breaking out of the while loop because no line was read.

#1 will only return one line of the temporary file, not the entire file. That’s because the return stops further execution of the function (and therefore the loop), so this function can only ever return one line if there is any data at all to be read. So it’s quite possible that if the JSON response returns multiple lines, you are trying to parse an incomplete response and that is crashing DKJSON.

#2 will only be taken if the temporary file is completely empty (e.g. the first attempt to read results in nothing). There is no return statement at all at the end of the function, so the function will return nothing (you will receive that as nil) and this will also result in DKJSON complaining.

It should also be noted that even if the Unifi system is returning a single-line JSON response, it may contain data that gives DKJSON heartburn. I would first go examine the contents of the temporary file. If it is empty, you’ve hit #2. If it is not empty, see if it has multiple lines; if so, you’ve hit #1. Otherwise, it’s possible that DKJSON is unhappy with the returned data (you might paste the data into jsonlint.com and examine it for special characters, particularly any special/international character that is not properly UTF-8 encoded).

Note: Also, because #1 exits the function early, the file handle is never closed and therefore in a plugin environment you would have a file handle leak that would eventually crash the system. My recommendations to fix your function:

  1. Replace the read("*line") with read("*all"). This will read the entire file in one pass. Note that as a general warning to doing it this way, very large responses will cause spikes in memory utilization that could destabilize LuaUPnP, but in this particular application, I doubt you’d ever see a response long enough.
  2. Remove the return line line from inside the while loop.
  3. Put return line at the end of your function (after the file:close() and right before the end statement).
  4. Declare all the variables (including line and file) that you are using in the function local. You’re making a ton of globals without that.

Thanks @rigpapa

There’s a lot to digest there. :grinning:

Every time I run the core code, with either a print or return statement I get a .json formatted response, and when I’ve placed that into an online json validator, it always reports good.

An earlier version of my code, that did not have the ‘break’ line, would just lock up and Vera would reload luup, so I had to add it so it could break out…

I don’t think it’s a massive .json file, (3000 lines on the jsonlint) and as you can see from the json designer image i posted - it holds a reasonable amount of values for each endpoint registered on the UniFi controller. I could not see anything out of the ordinary in the content produced.

As I can get a ‘json formatted file/response every time - is there something in Lua I could do to validate it ?

Also the error I always seem to get is on line 355, I looked at that via the json validator, but nothing looked wrong, although I have no idea how the script/code on Vera calculates the line number ? Do you ?

The cure is worse than the disease, in a way. If you read “*a” (or “*all”) it will never return nil: You don’t even need the while

local line
local file = io.open(tempfilename, "r")
if file then
    line = file:read("*a")
    file:close()
end
return line

The line number reported is in the code, not the JSON. You’d need to post the JSON for us to really be able to move forward.

Great, thanks so much @rigpapa - those updates helped, as using that updated core code…

local json = require "dkjson"
local unifidata = json.decode(UnifiController())
for k, v in pairs(unifidata) do
			print(k,v)
		end

… I now have two tables returned.

Here’s what LuaTest reported.

LuaTest 1.7

Lua file: /etc/cmh-ludl/luatest.lua

Results
No errors
Runtime: 309.7 ms
Code returned: nil

Print output
meta     table: 0x12c49b8     
data     table: 0xf05ec8     

Well, that’s looking up. At least it didn’t throw an error. The output isn’t useful, though, in looking at the data (can’t see what’s in the table), but if you know what’s in the table, mush on and extract what you need from it.

Yep, that is indeed what I’m trying to work out - how do I interrogate the resulting ‘data’ table…

I’ve spent some time trying to learn Lua and json decoding - with mixed success… it’s all seems to come down to how you break it up. For example.

Doing this seems to get me the keys - number of entries

for k, v in pairs(unifidata.data) do
print(k, v) 
end

Returns…

LuaTest 1.7

Lua file: /etc/cmh-ludl/luatest.lua

Results
No errors
Runtime: 1439.6 ms
Code returned: nil

Print output
1     table: 0x158e9a8     
2     table: 0x16189e0     
3     table: 0x1410898     
4     table: 0x14e6f68     
5     table: 0x149bb40   
Etc....

And then this returns all the variables.

local unifidata = json.decode(UnifiController())
	for k, v in pairs(unifidata.data) do
              for _, v in pairs(v) do
			print(v)
	end
end

But extracting a specific set of values is often where I struggle - any pointers ?

I’m finally making some progress with this, let’s see where it takes me :slight_smile:

local unifidata = json.decode(UnifiController())
	for key, value in pairs(unifidata.data) do
              for name, variable in pairs(value) do
	            	print(name, variable)
	end
end

Returns this.

LuaTest 1.7

Lua file: /etc/cmh-ludl/luatest.lua

Results
No errors
Runtime: 14803.9 ms
Code returned: nil

Print output
_is_guest_by_usw     false     
_last_seen_by_usw     1606773424     
sw_depth     1     
is_wired     true     
ip     192.168.1.134     
user_id     5f199b057de17904c996bc193    
sw_mac     78:8a:11:fe:1c:a8   
assoc_time     1606612980     
wired-tx_bytes     4708199785     
hostname     NAS247123     
wired-tx_bytes-r     4233     
wired-rx_packets     14381240     
latest_assoc_time     1606737061     
wired-rx_bytes-r     142     
first_seen     1595513605     
network_id     571b92fb384e11e6ac19093e     
network     LAN     
_id     5f199b057de17904c996bc192
wired-tx_packets     6544197     
wired-rx_bytes     20449918065     
is_guest     false     
oui     Qnap
etc..

Ok, I’ve gone as far as I can, I’m able to identify the values I want but my loops are wrong, as it shows me the same value for all the lines present - rather than just one line for each.

local json = require "dkjson"
local unifidata = json.decode(UnifiController())
	for key, value in pairs(unifidata.data) do
		for name, variable in pairs(value) do
                     print(value.ip, value.hostname, value.oui)
		end
	end

Returns…

LuaTest 1.7

Lua file: /etc/cmh-ludl/luatest.lua

Results
No errors
Runtime: 31906.8 ms
Code returned: nil

Print output
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.134     NAS41234AB     Qnap     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.237     nil     NestLabs     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb     
192.168.102.33     nil     Bskyb  
Etc...   

You are looping with: for name, variable in pairs(value) do

…but printing print(value.ip, value.hostname, value.oui)

So your inner loop is working but it prints value from the outer loop, that’s why it’s not changing.