A simple Envisalink implementation

Is anyone else aggravated with the DSC plugin? It just seems bulky and like it’s trying to do way more than I need. I only wanted to be able to say Alexa, goodbye and have my House Mode switch to Away - triggering scenes to adjust lights, A/C, and set the alarm. Then when I get home, I say Alexa, <previously unspoken sentence in human history> and the House Mode switches to Home - again triggering scenes to adjust lights, A/C, and disarming the alarm.

After days of fighting with the DSC plugin and even trying to find a way around the encrypted files just so I could see how it works, I gave up and found a copy of the Envisalink TIP Guide. The module I came up with has been reliable and so I decided to share in the hopes that someone else may find it does what they want without the need for the plugin:

You call it from a scene, passing parameters as a table. It has only one public function go() which requires only one argument provided you have hardcoded your defaults into the evl.lua file:

local evl = require "evl"
evl.go( { mode="away", } )

Here’s the module:

module("evl", package.seeall)

local luup = _G["luup"] or { log = print }	-- for running module off-vera
local _debug = false


--	Resolve a hostname
--
local function getipaddr (s)
	local socket = require "socket"
	local ipaddr = socket.dns.toip(s)
	return ipaddr
end


--	Calculate the checksum of a string and return the
--	lower 8 bits as a ASCII hex uppercase string
--
local function checksum (s)
	local c = 0
	for i = 1, #s do
		c = c + string.byte(s, i)
	end
	return(string.upper(string.sub(string.format("%x", c), -2)))
end


--	Format and send a command to the EVL
--
local function send (conn, cmd, dat)
	local s = cmd .. (dat or "")
	
	if _debug then luup.log("EVL < " .. s) end
	
	return conn:send(s .. checksum(s) .. "\r\n")
end


--	Read data from EVL, discarding the checksum,
--	then return a table with command and data
--
local function recv (conn)
	local s, err = conn:receive()
	if err then	return nil, err end
	
	if _debug then luup.log("EVL : " .. s) end
	
	return { cmd = string.sub(s, 1, 3), dat = string.sub(s, 4, -3) }
end


--	Send command and check for acknowledge reply from EVL
--
local function sendack (conn, cmd, dat)
	if not send(conn, cmd, dat) then return nil end
	
	local t, err = recv(conn)
	if err then return nil, err end
	
	return (t.cmd == "500") and (t.dat == cmd)
end


--	Do the login loop and return nil on success
--
local function login (conn, args)
	repeat
		local t, err = recv(conn)
		if err then	return "connection " .. err end
		
		if t.cmd == "505" then
			if		t.dat == "0" then return "password \"" .. args.evlpw .. "\" rejected"
			elseif	t.dat == "1" then return  nil
			elseif	t.dat == "2" then return "login timeout"
			elseif	t.dat == "3" then sendack(conn, "005", args.evlpw)
			end
		elseif t.cmd == "502" then
			return "evl system error " .. t.dat
		end
	until nil
end


--	Open session with EVL and set requested mode
--
--	Attempts to connect and login to the EVL using supplied args table.
--	Any args not passed will be set to defaults.
--
--	Notes:	evlpw is default 'user' on EVL3/4 and can be changed in	the
--			EVL web interface. Max length is 6 ASCII characters	on the
--			EVL3 and 10 on the EVL4.
--
--			If you haven't set a static IP for the EVL (either via it's
--			web interface or your router's DHCP), and it is on the same
--			subnet as your Vera, then "envisalink" should resolve OK.
--
--			Arm codes used here do not require an alarm code and report
--			as "Special Closing" from the panel
--
--			I have only tested this on an EVL4 but according to the TIP
--			guide, everything should be the same
--

function go (args)
	
	args = args or {}
	
	local modes = { away="030", stay="031", zedl="032", disarm="040", }
	
	
	--	You can hardcode your defaults here to
	--	avoid passing them from your scene
	--
	args.acode		= args.acode	or	"1234"
	args.evlpw		= args.evlpw	or	"user"
	args.host		= args.host		or	 getipaddr("envisalink")
	args.part		= args.part		or	"1"
	args.port		= args.port		or	 4025
	args.timeout	= args.timeout	or	 0.5

	
	if not args.host then
		luup.log("EVL: no host specified or failed to resolve");
		return false
		
	elseif not args.mode then
		luup.log("EVL: mode not specified")
		return false
		
	elseif not modes[args.mode] then
		luup.log("EVL: invalid mode specified: " .. args.mode)
		return false
	end	
	
	
	-- Establish and verify a connection
	local sock = require "socket"
	local conn = assert(sock.tcp())
	conn:settimeout(args.timeout)
	
	local _, err = conn:connect(args.host, args.port)
	if err then
		luup.log("EVL: failed to connect to " .. args.host .. ":" .. args.port .. " - " .. err)
		return false
	end
	
	
	-- Login
	err = login(conn, args)
	if err then
		luup.log("EVL: failed to login: " .. err)
		return false
	end


	-- If requesting disarm, append alarm code to partition number
	local dat = args.part .. (args.mode=="disarm" and args.acode or "")
	
	if sendack(conn, modes[args.mode], dat) then
		luup.log("EVL: \"" .. args.mode .. "\" command acknowledged")
	else
		luup.log("EVL: \"" .. args.mode .. "\" command not acknowledged")
	end
	
	conn:close()
end