Index: /trunk/vera-caddx/I_CaddxNX584Security.xml
===================================================================
--- /trunk/vera-caddx/I_CaddxNX584Security.xml	(revision 3)
+++ /trunk/vera-caddx/I_CaddxNX584Security.xml	(revision 3)
@@ -0,0 +1,1350 @@
+<?xml version="1.0"?>
+<implementation>
+	<settings>
+		<protocol>raw</protocol>
+	</settings>
+	<functions>
+--
+-- Luup plugin for GE/Networx/Caddx NX-584/NX-8E security system interface.
+--
+-- Version 0.1 2010-08-07 by Deborah Pickett
+-- * Probes interface for valid partitions
+-- * Sets interface clock
+-- * Creates luup child devices for partitions and zones
+-- * Tracks partitions' armed state (away/stay/disarmed/breached)
+-- * Tracks zones' tripped state
+-- * Tracks zones' bypass state
+-- * Actions supported:
+--   - bypass zone
+--   - quick (one-touch) arm
+--   - arm with PIN
+--   - quick (one-touch) partial (away) arm
+--   - partial (away) arm with PIN
+--   - disarm with PIN
+--
+
+ALARM_SERVICEID = "urn:schemas-futzle-com:serviceId:CaddxNX584Security1"
+ALARM_PARTITION_SERVICEID = "urn:micasaverde-com:serviceId:AlarmPartition1"
+
+INCOMING_EXPECTING_START = 0       -- Between messages
+INCOMING_EXPECTING_LENGTH = 1      -- Received start byte 0x7e
+INCOMING_EXPECTING_TYPE = 2        -- Received length byte
+INCOMING_EXPECTING_CHECKSUM1 = 3   -- Received all bytes, now need first checksum byte
+INCOMING_EXPECTING_CHECKSUM2 = 4   -- Received first checksum byte
+INCOMING_EXPECTING_MESSAGE = 5     -- Receiving message body bytes
+
+-- Interface doesn't know which zones are valid, so have to
+-- provide this information manually.
+-- To do: configure this with a variable.
+MAXIMUM_ZONE = 3 -- Highest-numbered zone in system.
+
+LOG_DEBUG = true -- Debug I/O with interface.
+
+--
+-- Utility functions for bitwise operations.
+--
+
+-- Thanks to guessed for this snippet.
+function findChild(deviceId, label)
+	for k, v in pairs(luup.devices) do
+		if (v.device_num_parent == deviceId and v.id == label) then
+			return k
+		end
+	end
+end
+
+-- bitMask(val, pos)
+-- Poor man's bitwise operation: return true if a bit is set, false if clear.
+-- val: number from 0 to 255
+-- pos: power of two, value of bit being tested
+function bitMask(val, pos)
+	return (val % (pos*2) &gt;= pos)
+end
+
+-- bitAnd(a, b)
+-- poor man's bitwise operation: AND of two bytes.
+-- a, b: numbers 0-255 to compute the bitwise AND of
+function bitAnd(a, b)
+	local result = 0
+	local pos = 1
+	repeat
+		if (bitMask(a, pos) and bitMask(b,pos)) then
+			result = result + pos
+		end
+		pos = pos * 2
+	until pos == 256
+end
+
+
+--
+-- Initial setup
+--
+
+-- caddxInitialize(deviceId)
+-- Initialize the interface:
+-- - Check that the interface is configured to handle all the requests we may send.
+-- - Get status of the alarm system.
+-- - Get a list of valid partitions.
+function caddxInitialize(deviceId)
+	luup.log("Initializing Caddx NX-584")
+	luup.io.intercept()
+
+	-- Incoming byte state machine initialization.
+	RECEIVE_STATE = INCOMING_EXPECTING_START
+	INCOMING_ESCAPED = 0x0
+	CHECKSUM1 = 0
+	CHECKSUM2 = 0
+	LENGTH = 0
+
+	-- Ask the alarm system if it's configured to be
+	-- able to respond to the requests we'll give it.
+	setUpInterface(deviceId)
+
+	-- Set the clock on the interface.
+	if (CAPABILITY_SET_CLOCK) then
+		setInterfaceClock()
+	end
+
+	-- Ask the alarm system about the global status.
+	-- Includes: faults; 4- or 6-digit PIN; list of valid partitions.
+	getSystemStatus(deviceId)
+
+	-- Get the names of all zones (even invalid ones; we will
+	-- winnow out the invalid ones later).
+	ZONE_STATUS = {}
+	for zone = 1,MAXIMUM_ZONE do
+		-- Pick a default name in case we can't get the zone's real name.
+		ZONE_STATUS[zone] = {
+			["name"] = "Zone " .. zone
+		}
+		if (CAPABILITY_ZONE_NAME) then
+			-- Ask the interface for the zone name.
+			if (LOG_DEBUG) then luup.log("Getting zone name for " .. zone) end
+			repeat until getZoneName(deviceId, zone)
+		end
+	end
+
+	-- To do: Start enumerating child devices.
+	local childDevices = luup.chdev.start(deviceId)
+
+	-- Get information about each partition.
+	PARTITION_STATUS = {}
+	PARTITION_CONFIGURED = {}
+	for partition, _ in pairs(PARTITION_VALID) do
+		PARTITION_CONFIGURED[partition] = false
+		PARTITION_STATUS[partition] = {}
+	end
+	repeat
+		-- Do this until we are satisfied that we are done.
+		-- This is complicated by the fact that the alarm system
+		-- may send asynchronous events while we are learning its
+		-- configuration.  Perversely, we may get an asynchronous
+		-- message about partition x when we just asked about partition y.
+		local done = true
+		for partition, configured in pairs(PARTITION_CONFIGURED) do
+			if (not configured) then
+				if (LOG_DEBUG) then luup.log("Setting up partition " .. partition) end
+				done = false
+				if (setUpPartition(deviceId, childDevices, partition)) then
+					PARTITION_CONFIGURED[partition] = true
+				end
+			end
+		end
+	until done == true
+
+	-- Get information about each zone.
+	-- Includes: zone valid
+	ZONE_CONFIGURED = {}
+	ZONE_VALID = {}
+	for zone = 1,MAXIMUM_ZONE do
+		ZONE_CONFIGURED[zone] = false
+		ZONE_STATUS[zone] = {}
+	end
+	repeat
+		-- Do this until we are satisfied that we are done.
+		-- The same caveats apply to zone as they do partitions.
+		local done = true
+		for zone, configured in pairs(ZONE_CONFIGURED) do
+			if (not configured) then
+				if (LOG_DEBUG) then luup.log("Setting up zone " .. zone) end
+				done = false
+				if (setUpZone(deviceId, childDevices, zone)) then
+					ZONE_CONFIGURED[zone] = true
+				end
+			end
+		end
+	until done == true
+
+	-- Commit child devices.
+	luup.chdev.sync(deviceId, childDevices)
+
+	-- Set the initial states for each partition based on the
+	-- information we've collected.
+	for partition, _ in pairs(PARTITION_VALID) do
+		updatePartitionDevice(deviceId, partition)
+	end
+
+	-- Set the initial states for each zone based on the
+	-- information we've collected.
+	for zone, _ in pairs(ZONE_VALID) do
+		updateZoneDevice(deviceId, zone)
+	end
+
+	-- Setup is complete.  Prepare to finish initialization.
+	if (LOG_DEBUG) then luup.log("Finished initialization") end
+
+	-- These are the messages that we expect to get from the alarm system
+	-- during normal operation.
+	PERMANENT_HANDLERS = {
+		[4] = handleZoneStatusMessage,
+		[5] = handleZonesSnapshotMessage,
+		[6] = handlePartitionStatusMessage,
+		[7] = handlePartitionsSnapshotMessage,
+		[8] = handleSystemStatusMessage,
+	}
+	TEMPORARY_HANDLERS = {}
+
+	JOBS = {}
+	JOBS_PENDING_SEND = {}
+	JOBS_PENDING_SEND_HEAD = 1
+	JOBS_PENDING_SEND_TAIL = 0
+	
+	-- Initializtion complete.
+end
+
+-- sendMessageAndHandleResponse(deviceId, message, handlers)
+-- Send a message (message type + body encoded in a string)
+-- to the alarm system, and wait for a message in response.
+function sendMessageAndHandleResponse(deviceId, message, handlers)
+	luup.io.intercept()
+	local retries = 0
+	-- Todo: might need to re-send outgoing message.
+	sendMessage(message)
+	repeat
+		-- Get a full message.
+		local a, b, c
+		repeat
+			local status
+			status, a, b, c = readByte(luup.io.read())
+			if (status == 2) then
+				-- Bad checksum.  Send a negative acknowledgment message.
+				sendNegativeAcknowledgeMessage()
+				retries = retries + 1
+			end
+		until (status == 4)
+		if (handlers[a] ~= nil) then
+			-- This is the message I am looking for.
+			return handleMessage(deviceId, handlers, a, b, c)
+		else
+			-- This is a message I don't want.
+			if (LOG_DEBUG) then luup.log(string.format("Received inconvenient message 0x%02x", a)) end
+			if (LOG_DEBUG) then logMessage("Unsolicited message body:", b) end
+			-- Implementation note: documentation says we should send
+			-- sendMessageRejectedMessage() but that doesn't seem to placate
+			-- the alarm system.
+			if (c) then sendPositiveAcknowledgeMessage() end  -- Yes, dear.
+			sendMessage(message)
+			retries = retries + 1
+		end
+	until (retries == MAX_RETRIES)
+	-- TODO: Set failure on the luup device.
+end
+
+-- setUpInterface(deviceId)
+-- Get the configuration information about the alarm system.
+-- Send an Interface Configuration Request 0x21,
+-- and wait for the reply 0x01.  Reject any other messages
+-- that may come in (e.g., zone status messages)
+function setUpInterface(deviceId)
+	if (LOG_DEBUG) then luup.log("Sending message and waiting for response: 0x21 Interface Configuration Request") end
+
+	sendMessageAndHandleResponse(deviceId, "\033", 
+		{
+			[1] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x01 Interface Configuration") end
+
+				-- Firmware version.
+				if (LOG_DEBUG) then luup.log(string.format("Firmware version %s", string.sub(message, 1, 4))) end
+
+				-- Check that the interface can respond to the message requests
+				-- that we need.
+				if (bitMask(string.byte(string.sub(message,5)), 2) -- 0x01
+					and bitMask(string.byte(string.sub(message,5)), 16) -- 0x04
+					and bitMask(string.byte(string.sub(message,5)), 32) -- 0x05
+					and bitMask(string.byte(string.sub(message,5)), 64) -- 0x06
+					and bitMask(string.byte(string.sub(message,5)), 128) -- 0x07
+					and bitMask(string.byte(string.sub(message,6)), 1) -- 0x08
+					and bitMask(string.byte(string.sub(message,7)), 2) -- 0x21
+					and bitMask(string.byte(string.sub(message,7)), 16) -- 0x24
+					and bitMask(string.byte(string.sub(message,7)), 32) -- 0x25
+					and bitMask(string.byte(string.sub(message,7)), 64) -- 0x26
+					and bitMask(string.byte(string.sub(message,7)), 128) -- 0x27
+					and bitMask(string.byte(string.sub(message,8)), 1) -- 0x28
+				) then
+					luup.log("All message codes are supported.")
+				else
+					-- An essential message has been disabled.
+					-- To do: signal failure.
+				end
+
+				-- Not all alarm systems know how to name zones; it depends
+				-- on the keypads attached to the system.
+				if (bitMask(string.byte(string.sub(message,7)), 8)) then
+					luup.log("Zone Name enabled")
+					CAPABILITY_ZONE_NAME = true
+				end
+
+				-- Set Clock can be optionally disabled.
+				if (bitMask(string.byte(string.sub(message,10)), 8)) then
+					luup.log("Set Clock enabled")
+					CAPABILITY_SET_CLOCK = true
+				end
+
+				-- Primary Keypad Function with PIN can be optionally disabled.
+				if (bitMask(string.byte(string.sub(message,10)), 16)) then
+					luup.log("Primary Keypad Function with PIN enabled")
+					CAPABILITY_PRIMARY_KEYPAD_WITH_PIN = true
+				end
+
+				-- Secondary Keypad Function can be optionally disabled.
+				if (bitMask(string.byte(string.sub(message,10)), 64)) then
+					luup.log("Secondary Keypad Function enabled")
+					CAPABILITY_SECONDARY_KEYPAD = true
+				end
+
+				-- Zone bypass can be optionally disabled.
+				if (bitMask(string.byte(string.sub(message,10)), 128)) then
+					luup.log("Zone bypass enabled")
+					CAPABILITY_ZONE_BYPASS = true
+				end
+
+				return 0
+			end
+		}
+	)
+end
+
+-- setInterfaceClock()
+-- Tell the alarm system the current time.  It uses this when
+-- it communicates with the back-to-base monitoring service
+-- and it may display it on alphanumeric keypads.
+function setInterfaceClock()
+	local timeTable = os.date("*t")
+	sendMessageAndHandleResponse(deviceId, string.char(0x3b + 128) .. string.format("%c%c%c%c%c%c",
+			timeTable["year"] % 100,
+			timeTable["month"],
+			timeTable["day"],
+			timeTable["hour"],
+			timeTable["min"],
+			timeTable["wday"]),
+		{
+			[29] = function (deviceId, message)
+				return 0
+			end,
+			[31] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Cannot set clock on interface") end
+				return 0
+			end,
+		}
+	)
+end
+
+-- getSystemStatus(deviceId)
+-- Request the state of the alarm system:
+-- - List of valid partitions.
+-- - 4- or 6-digit PIN codes.
+function getSystemStatus(deviceId)
+	if (LOG_DEBUG) then luup.log("Sending message and waiting for response: 0x28 System Status Request") end
+
+	PARTITION_VALID = {}
+
+	-- Callbacks to handle System Status request.
+	sendMessageAndHandleResponse(deviceId, "\040", 
+		{
+			[8] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x08 System Status") end
+
+				-- Byte 10 of the response contains a list of valid partitions.
+				VALID_PARTITIONS_BITMASK = string.byte(string.sub(message,10))
+				for partition = 1, 8 do
+					if (bitMask(VALID_PARTITIONS_BITMASK, 2^(partition-1))) then
+						luup.log(string.format("Valid partition %d", partition))
+						PARTITION_VALID[partition] = true
+					end
+				end
+
+				-- Byte 6 contains 4- or 6-digit PINs
+				if (bitMask(string.byte(string.sub(message,6)), 1)) then
+					CONFIGURATION_PIN_LENGTH = 6
+				else
+					CONFIGURATION_PIN_LENGTH = 4
+				end
+				luup.log(string.format("PIN length is %d", CONFIGURATION_PIN_LENGTH))
+
+				return 0
+			end
+		}
+	)
+end
+
+-- getZoneName(z)
+-- The plugin would like to know the name of a given zone
+-- (z, integer) in a Zone Name message (0x03).
+-- This isn't implemented on the NX-4 so I haven't been able to test it.
+function getZoneName(deviceId, z)
+	if (LOG_DEBUG) then luup.log("Sending message and waiting for response: 0x23 Zone Name Request") end
+	local zoneExpected = false
+	sendMessageAndHandleResponse(deviceId, "\035" .. string.char(z - 1), 
+		{
+			[3] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x03 Zone Name") end
+				-- First byte is the zone number.
+				local zoneNumber = string.byte(string.sub(message,1)) + 1
+				if (z ~= zoneNumber) then
+					-- This isn't the zone I was asking about.
+					return 0
+				end
+				-- Next 16 bytes are the zone name.
+				local zoneName = string.gsub(string.sub(message,2,17), "%z", "")
+				if (zoneName == "") then
+					-- No name, so stop trying.
+					zoneExpected = true
+					return 0
+				end
+				-- Use this zone name instead of the default.
+				ZONE_STATUS[z]["name"] = zoneName
+				zoneExpected = true;
+				return 0
+			end,
+			[31] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x1F Message Reject") end
+				-- The interface can't give me a zone name after all.
+				zoneExpected = true
+				return 0
+			end
+		}
+	)
+	return zoneExpected
+end
+
+-- setUpPartition(deviceId, childDevices, p)
+-- p: number, partition (1 origin)
+-- Returns true if the received reply was for the requested partition,
+-- otherwise false (and the caller should try again).
+function setUpPartition(deviceId, childDevices, p)
+	if (LOG_DEBUG) then luup.log("Sending message and waiting for response: 0x26 Partition Status Request") end
+
+	local partitionConfigured = false
+
+	sendMessageAndHandleResponse(deviceId, "\038" .. string.char(p - 1),
+		{
+			[6] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x06 Partition Status") end
+				if (string.byte(string.sub(message,1)) == p - 1) then
+					-- This is the partition we were asking about.
+					-- Partitions aren't named, so invent a suitable name.
+					PARTITION_STATUS[p]["name"] = "Partition " .. p
+					luup.chdev.append(
+						deviceId, childDevices,
+						"Partition-" .. p, PARTITION_STATUS[p]["name"],
+						"urn:schemas-micasaverde-com:device:AlarmPartition:1", "D_AlarmPartition1.xml",
+						"", "", true
+					)
+					processPartitionStatusMessage(message)
+					partitionConfigured = true
+				else
+					-- This partition may have been configured already.
+					-- May as well note the changed state.
+					processPartitionStatusMessage(message)
+				end
+				return 0
+			end,
+			[7] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x07 Partitions Snapshot") end
+				-- This isn't a response to our request, but we should
+				-- note the status changes for the paritions that we've
+				-- already processed.
+				processPartitionsSnapshotMessage(message)
+				return 0
+			end
+		}
+	)
+	return partitionConfigured
+end
+
+-- setUpZone(deviceId, childDevices, z)
+-- Get information about a zone and create a child device for it.
+-- z: Zone number (1 origin)
+-- Returns true if received the configuration for the requested zone,
+-- otherwise returns false (and caller should try again).
+function setUpZone(deviceId, childDevices, z)
+	if (LOG_DEBUG) then luup.log("Sending message and waiting for response: 0x24 Zone Status Request") end
+	sendMessageAndHandleResponse(deviceId, "\036" .. string.char(z - 1), 
+		{
+			[4] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x04 Zone Status") end
+				if (string.byte(string.sub(message,1)) == z - 1) then
+					-- This is the zone we were asking about.
+					luup.chdev.append(
+						deviceId, childDevices,
+						"Zone-" .. z, ZONE_STATUS[z]["name"],
+						"urn:schemas-micasaverde-com:device:MotionSensor:1", "D_MotionSensor1.xml",
+						"I_CaddxNX584Security.xml", "", false
+					)
+					processZoneStatusMessage(message)
+					zoneConfigured = true
+				else
+					-- This isn't the zone I asked about, but I may know about it.
+					if (ZONE_CONFIGURED[string.byte(string.sub(message,1)) + 1]) then
+						processZoneStatusMessage(message)
+					end
+				end
+				return 0
+			end,
+			[5] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x05 Zones Snapshot") end
+				-- Process the zones snapshot message, for the zones
+				-- which have been configured.
+				processZonesSnapshotMessage(message)
+				return 0
+			end,
+			[6] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x06 Partition Status") end
+				processPartitionStatusMessage(message)
+				return 0
+			end,
+			[7] = function (deviceId, message)
+				if (LOG_DEBUG) then luup.log("Handling message: 0x07 Partitions Snapshot") end
+				processPartitionsSnapshotMessage(message)
+				return 0
+			end
+		}
+	)
+	return zoneConfigured
+end
+
+--
+-- Shared code for processing messages during setup and normal operation.
+--
+
+-- processPartitionStatusMessage(message)
+-- Update the state of a child device upon receipt of a 0x06 Partition Status message.
+-- message: The body of the message.  The partition must already be configured.
+function processPartitionStatusMessage(message)
+	local partition = string.byte(string.sub(message,1)) + 1
+	PARTITION_STATUS[partition]["isArmed"] = bitMask(string.byte(string.sub(message,2)), 64)
+	PARTITION_STATUS[partition]["isPartial"] = bitMask(string.byte(string.sub(message,4)), 4)
+	PARTITION_STATUS[partition]["isSiren"] = bitMask(string.byte(string.sub(message,3)), 4)
+	PARTITION_STATUS[partition]["isReady"] = bitMask(string.byte(string.sub(message,7)), 4)
+	PARTITION_STATUS[partition]["lastUser"] = string.byte(string.sub(message,6))
+	return partition
+end
+
+-- processPartitionsSnapshotMessage(message)
+-- Update the state of all child partition devices that are configured.
+function processPartitionsSnapshotMessage(message)
+	for partition = 1,8 do
+		if (PARTITION_VALID[parition] and PARTITION_CONFIGURED[partition]) then
+			PARTITION_STATUS[partition]["isArmed"] = bitMask(string.byte(string.sub(message,partition+1)), 4)
+			PARTITION_STATUS[partition]["isPartial"] = bitMask(string.byte(string.sub(message,partition+1)), 8)
+			PARTITION_STATUS[partition]["isReady"] = bitMask(string.byte(string.sub(message,partition+1)), 2)
+		end
+	end
+end
+
+-- processZoneStatusMessage(message)
+-- Update the state of a child device upon receipt of a 0x04 Zone Status message.
+-- message: The body of the message.  The zone must already be configured.
+function processZoneStatusMessage(message)
+	local zone = string.byte(string.sub(message,1)) + 1
+	local partitions = bitAnd(string.byte(string.sub(message,2)), VALID_PARTITIONS_BITMASK)
+	if (partitions ~= 0) then
+		if (LOG_DEBUG) then luup.log(string.format("Valid zone %d", zone)) end
+		ZONE_VALID[zone] = true
+		ZONE_STATUS[zone]["isFaulted"] = bitMask(string.byte(string.sub(message,6)), 1)
+		ZONE_STATUS[zone]["isBypassed"] = bitMask(string.byte(string.sub(message,6)), 8)
+	end
+	return zone
+end
+
+-- processZonesSnapshotMessage(message)
+-- Update the state of all child partition devices that are configured.
+function processZonesSnapshotMessage(message)
+	-- First byte is the set of 16 zones in this snapshot.
+	local z16 = string.byte(string.sub(message,2)) * 16
+	for zone2 = 1,8 do
+		-- Two zones per byte.
+		if (ZONE_VALID[z16+zone2*2-1] and ZONE_CONFIGURED[z16+zone2*2-1]) then
+			ZONE_STATUS[z16+zone2*2-1]["isFaulted"] = bitMask(string.byte(string.sub(message,zone2+1)), 1)
+			ZONE_STATUS[z16+zone2*2-1]["isBypassed"] = bitMask(string.byte(string.sub(message,zone2+1)), 2)
+		end
+		if (ZONE_VALID[z16+zone2*2] and ZONE_CONFIGURED[z16+zone2*2]) then
+			ZONE_STATUS[z16+zone2*2]["isFaulted"] = bitMask(string.byte(string.sub(message,zone2+1)), 16)
+			ZONE_STATUS[z16+zone2*2]["isBypassed"] = bitMask(string.byte(string.sub(message,zone2+1)), 32)
+		end
+	end
+	return z16
+end
+
+-- updatePartitionDevice(deviceId, partition)
+-- Update the Luup child device corresponding to the partition
+-- with information previously set in the PARTITION_STATUS variable.
+function updatePartitionDevice(deviceId, partition)
+	if (partition ~= nil) then
+		if (LOG_DEBUG) then luup.log("Setting state for partition " .. partition) end
+		local partitionDevice = findChild(deviceId, "Partition-" .. partition)
+
+		local armed = PARTITION_STATUS[partition]["isArmed"] and "1" or "0"
+		if (LOG_DEBUG) then luup.log("Armed: " .. armed) end
+		if (luup.variable_get(ALARM_PARTITION_SERVICEID, "Armed", partitionDevice) ~= armed) then
+			luup.variable_set(ALARM_PARTITION_SERVICEID, "Armed", armed, partitionDevice)
+		end
+
+		local stay = PARTITION_STATUS[partition]["isPartial"] and not PARTITION_STATUS[partition]["isArmed"] and "1" or "0"
+		if (LOG_DEBUG) then luup.log("StayArmed: " .. stay) end
+		if (luup.variable_get(ALARM_PARTITION_SERVICEID, "StayArmed", partitionDevice) ~= stay) then
+			luup.variable_set(ALARM_PARTITION_SERVICEID, "StayArmed", stay, partitionDevice)
+		end
+
+		local disarmed = not PARTITION_STATUS[partition]["isArmed"] and not PARTITION_STATUS[partition]["isPartial"] and "1" or "0"
+		if (LOG_DEBUG) then luup.log("Disarmed: " .. disarmed) end
+		if (luup.variable_get(ALARM_PARTITION_SERVICEID, "Disarmed", partitionDevice) ~= disarmed) then
+			luup.variable_set(ALARM_PARTITION_SERVICEID, "Disarmed", disarmed, partitionDevice)
+		end
+
+		local breached = PARTITION_STATUS[partition]["isSiren"] and "1" or "0"
+		if (LOG_DEBUG) then luup.log("Breached: " .. breached) end
+		if (luup.variable_get(ALARM_PARTITION_SERVICEID, "Breached", partitionDevice) ~= breached) then
+			luup.variable_set(ALARM_PARTITION_SERVICEID, "Breached", breached, partitionDevice)
+		end
+
+		-- Create a variable on the alarm interface matching the user who
+		-- last changed the partition, if there isn't already.
+		if (luup.variable_get(ALARM_SERVICEID, "User" .. PARTITION_STATUS[partition]["lastUser"], deviceId) == nil) then
+			luup.variable_set(ALARM_SERVICEID, "User" .. PARTITION_STATUS[partition]["lastUser"], "User " .. PARTITION_STATUS[partition]["lastUser"], deviceId)
+		end
+		-- This is the user who last modified the partition.
+		luup.variable_set(ALARM_PARTITION_SERVICEID, "LastUser", (PARTITION_STATUS[partition]["lastUser"]), partitionDevice)
+	end
+end
+
+-- updateZoneDevice(deviceId, zone)
+-- Update the Luup child device corresponding to the zone
+-- with information previously set in the ZONE_STATUS variable.
+function updateZoneDevice(deviceId, zone)
+	if (zone ~= nil) then
+		if (LOG_DEBUG) then luup.log("Setting state for zone " .. zone) end
+		local zoneDevice = findChild(deviceId, "Zone-" .. zone)
+
+		local tripped = ZONE_STATUS[zone]["isFaulted"] and "1" or "0"
+		if (LOG_DEBUG) then luup.log("Tripped: " .. tripped) end
+		if (luup.variable_get("urn:micasaverde-com:serviceId:SecuritySensor1", "Tripped", zoneDevice) ~= tripped) then
+			luup.variable_set("urn:micasaverde-com:serviceId:SecuritySensor1", "Tripped", tripped, zoneDevice)
+		end
+
+		-- Invert logic because alarm panel speaks of "is bypassed".
+		local armed = ZONE_STATUS[zone]["isBypassed"] and "0" or "1"
+		if (LOG_DEBUG) then luup.log("Armed: " .. armed) end
+		if (luup.variable_get("urn:micasaverde-com:serviceId:SecuritySensor1", "Armed", zoneDevice) ~= armed) then
+			luup.variable_set("urn:micasaverde-com:serviceId:SecuritySensor1", "Armed", armed, zoneDevice)
+		end
+	end
+end
+
+--
+-- Handlers for asynchronous messages from the alarm system.
+--
+
+-- handleZoneStatusMessage(deviceId, message)
+-- We received a zone status message.
+-- Use the information in it to update the zone device.
+function handleZoneStatusMessage(deviceId, message)
+	if (LOG_DEBUG) then luup.log("Handling message: 0x04 Zone Status") end
+	local zone = processZoneStatusMessage(message)
+	updateZoneDevice(deviceId, zone)
+        return 0
+end
+
+-- handleZonesSnapshotMessage(deviceId, message)
+-- We received a zones snapshot message for a bank of 16 zones.
+-- Use the information in it to update the zone devices.
+function handleZonesSnapshotMessage(deviceId, message)
+	if (LOG_DEBUG) then luup.log("Handling message: 0x05 Zones Snapshot") end
+	local z16 = processZonesSnapshotMessage(message)
+	for zone = z16*16+1,z16*16+16 do
+		if (ZONE_VALID[zone]) then
+			updateZoneDevice(deviceId, zone)
+		end
+	end
+        return 0
+end
+
+-- handlePartitionStatusMessage(deviceId, message)
+-- We received a partition status message.
+-- Use the information in it to update the partition device.
+function handlePartitionStatusMessage(deviceId, message)
+	if (LOG_DEBUG) then luup.log("Handling message: 0x06 Partition Status") end
+	local partition = processPartitionStatusMessage(message)
+	updatePartitionDevice(deviceId, partition)
+        return 0
+end
+
+-- handlePartitionsSnapshotMessage(deviceId, message)
+-- We received a partitions snapshot message for all eight partitions.
+-- Use the information in it to update the partition devices.
+function handlePartitionsSnapshotMessage(deviceId, message)
+	if (LOG_DEBUG) then luup.log("Handling message: 0x07 Partitions Snapshot") end
+	processPartitionsSnapshotMessage(message)
+	for parition = 1,8 do
+		if (PARTITION_VALID[partition]) then
+			updatePartitionDevice(deviceId, partition)
+		end
+	end
+        return 0
+end
+
+function handleSystemStatusMessage()
+	if (LOG_DEBUG) then luup.log("Handling message: 0x08 System Status") end
+	-- Anything to do? We shouldn't be getting this message spontaneously, should we?
+        return 0
+end
+
+
+--
+-- Utility functions for communicating with the interface.
+--
+
+-- sendCommandFailedMessage()
+-- Send Command Failed if plugin was unable to do something
+-- asked of it by the alarm system.
+function sendCommandFailedMessage()
+	if (LOG_DEBUG) then luup.log("Sending message: 0x1C Command Failed") end
+	sendMessage("\028") -- 28 == 0x1C
+end
+
+-- sendPositiveAcknowledgeMessage()
+-- Send Positive Acknowledgment if plugin was asked to
+-- acknowledge something by the alarm system.
+function sendPositiveAcknowledgeMessage()
+	if (LOG_DEBUG) then luup.log("Sending message: 0x1D Positive Acknowledge") end
+	sendMessage("\029") -- 29 == 0x1D
+end
+
+-- sendNegativeAcknowledgeMessage()
+-- Send Negative Acknowledgment if plugin received a garbled
+-- message or if the alarm system tries to send a second
+-- message before the first is acknowledged.
+function sendNegativeAcknowledgeMessage()
+	if (LOG_DEBUG) then luup.log("Sending message: 0x1E Negative Acknowledge") end
+	sendMessage("\030") -- 30 == 0x1E
+end
+
+-- sendMessageRejectedMessage()
+-- Send Message Rejection if plugin received a message,
+-- it is valid, but the interface doesn't know what to
+-- do with it.
+-- Experience suggests the interface doesn't expect to
+-- receive this message, but only to send it.
+function sendMessageRejectedMessage()
+	if (LOG_DEBUG) then luup.log("Sending message: 0x1F Message Reject") end
+	sendMessage("\031") -- 31 == 0x1F
+end
+
+-- packByte(b)
+-- Escape bytes 0x7e and 0x7d to ensure that the start byte (0x7e)
+-- is never seen in the middle of a message.
+function packByte(b)
+	if (b == 0x7e) then
+		return "\125\094" -- 0x7d 0x5e
+	elseif (b == 0x7d) then
+		return "\125\093" -- 0x7d 0x5d
+	else
+		return string.char(b)
+	end
+end
+
+-- sendMessage(message)
+-- Byte-stuff the message, prepend the start byte 0x7e and the length,
+-- append the checksum, and send it.
+function sendMessage(message)
+	local packedMessage = string.char(0x7e)
+	resetChecksum()
+	-- Length should be always short enough to avoid byte-stuffing issues.
+	packedMessage = packedMessage .. string.char(string.len(message))
+	updateChecksum(string.len(message))
+	for i = 1, string.len(message) do
+		local b = string.byte(message,i)
+		updateChecksum(b)
+		packedMessage = packedMessage .. packByte(b)
+	end
+	local c1, c2 = getChecksum()
+	packedMessage = packedMessage .. packByte(c1) .. packByte(c2)
+	if (LOG_DEBUG) then logMessage("Outgoing:", packedMessage) end
+	luup.io.write(packedMessage)
+end
+
+function resetChecksum()
+	CHECKSUM1 = 0
+	CHECKSUM2 = 0
+end
+
+function updateChecksum(b)
+	if (255 - CHECKSUM1 &lt; b) then
+		CHECKSUM1 = CHECKSUM1 + 1
+	end
+	CHECKSUM1 = CHECKSUM1 + b
+	if (CHECKSUM1 &gt; 255) then
+		CHECKSUM1 = CHECKSUM1 - 256
+	end
+	if (CHECKSUM1 == 255) then
+		CHECKSUM1 = 0
+	end
+	if (255 - CHECKSUM2 &lt; CHECKSUM1) then
+		CHECKSUM2 = CHECKSUM2 + 1
+	end
+	CHECKSUM2 = CHECKSUM2 + CHECKSUM1
+	if (CHECKSUM2 &gt; 255) then
+		CHECKSUM2 = CHECKSUM2 - 256
+	end
+	if (CHECKSUM2 == 255) then
+		CHECKSUM2 = 0
+	end
+end
+
+function getChecksum()
+	return CHECKSUM1, CHECKSUM2
+end
+
+-- handleMessage(deviceId, handlers, messageType, message, acknowledge)
+-- Figure out which function should handle this message, and call that function
+-- with the message.
+function handleMessage(deviceId, handlers, messageType, message, acknowledge)
+	if (acknowledge) then
+		if (LOG_DEBUG) then luup.log(string.format("Received good message 0x%02x, acknowledge requested", messageType)) end
+	else
+		if (LOG_DEBUG) then luup.log(string.format("Received good message 0x%02x", messageType)) end
+	end
+	if (LOG_DEBUG) then logMessage("Incoming message body:", message) end
+	if (handlers[messageType] ~= nil) then
+		local f = handlers[messageType]
+		local status = f(deviceId, message)
+		if (status == 0 and acknowledge) then
+			sendPositiveAcknowledgeMessage()
+		elseif (acknowledge) then
+			sendCommandFailedMessage()
+		end
+	else
+		sendMessageRejectedMessage()
+	end
+end
+
+-- logMessage(prefix, message)
+-- Send to the log the bytes that make up the message.
+function logMessage(prefix, message)
+	logText = prefix
+	for i = 1, string.len(message) do
+		local b = string.byte(message,i)
+		logText = logText .. string.format(" 0x%02x", b)
+	end
+	luup.log("Message: " .. logText)
+end
+
+-- readByte(lul_data)
+-- Given the next byte of input (lul_data),
+-- return the next message encoded in the input.
+-- Return values:
+-- - Message complete: 4, messageId, messageBody, acknowledgeRequired
+-- - Message not yet complete: 5
+-- - Bad checksum: 2, expectedChecksum, receivedChecksum
+-- - Garbage byte outside message: 3, byteReceived
+function readByte(lul_data)
+	if (RECEIVE_STATE == nil) then
+		if (LOG_DEBUG) then luup.log("State machine not yet initialized") end
+		return
+	end
+	if (lul_data == nil) then
+		if (LOG_DEBUG) then luup.log("Input is nil") end
+		return
+	end
+	local b = string.byte(lul_data)
+	if (b == 0x7e) then
+		-- Start of a message.
+		if (RECEIVE_STATE ~= INCOMING_EXPECTING_START) then
+			-- Start byte should occur only at the start of a message,
+			-- so assume previous message is truncated and start again.
+			if (LOG_DEBUG) then luup.log("Discarding previous incomplete message") end
+		end
+		resetChecksum()
+		RECEIVE_STATE = INCOMING_EXPECTING_LENGTH
+		INCOMING_ESCAPED = 0x0
+		return 5 -- Message incomplete.
+	elseif (RECEIVE_STATE == INCOMING_EXPECTING_START) then
+		-- Garbage byte (or ASCII mode).
+		if (LOG_DEBUG) then luup.log(string.format("Ignoring byte %02x", b)) end
+		return 3, b -- Garbage byte
+	elseif (b == 0x7d) then
+		-- First byte of a byte-stuffed pair.
+		INCOMING_ESCAPED = 0x20
+		return 5
+	else
+		-- Everything from here on participates in the checksum computation.
+		b = b + INCOMING_ESCAPED  -- Either 0 (or 0x20 if second byte of a byte-stuffed pair)
+		INCOMING_ESCAPED = 0x0
+		updateChecksum(b)
+		if (RECEIVE_STATE == INCOMING_EXPECTING_LENGTH) then
+			-- Length includes message type, message and checksum.
+			LENGTH = b
+			RECEIVE_STATE = INCOMING_EXPECTING_TYPE
+		else
+			LENGTH = LENGTH - 1
+			if (RECEIVE_STATE == INCOMING_EXPECTING_TYPE) then
+				-- Type is format a0xxxxxx:
+				-- a == 0 means do not acknowledge the message.
+				-- a == 1 means acknowledge the message.
+				-- xxxxxx is the message number (0 to 63).
+				if (b &gt; 127) then
+					ACKNOWLEDGE = true
+				else
+					ACKNOWLEDGE = false
+				end
+				MESSAGE_NUMBER = b % 64
+				RECEIVE_STATE = INCOMING_EXPECTING_MESSAGE
+				MESSAGE = ""
+			elseif (RECEIVE_STATE == INCOMING_EXPECTING_MESSAGE) then
+				-- Message body.
+				MESSAGE = MESSAGE .. string.char(b)
+			elseif (RECEIVE_STATE == INCOMING_EXPECTING_CHECKSUM1) then
+				-- First byte of checksum.
+				RECEIVED_CHECKSUM1 = b
+				RECEIVE_STATE = INCOMING_EXPECTING_CHECKSUM2
+			elseif (RECEIVE_STATE == INCOMING_EXPECTING_CHECKSUM2) then
+				-- First byte of checksum.
+				if (EXPECTED_CHECKSUM1 == RECEIVED_CHECKSUM1 and
+					EXPECTED_CHECKSUM2 == b) then
+					-- Good checksum.
+					RECEIVE_STATE = INCOMING_EXPECTING_START
+					return 4, 
+						MESSAGE_NUMBER,
+						MESSAGE,
+						ACKNOWLEDGE
+				else
+					-- Bad checksum.  Send Negative Acknowledge (0x1e)
+					if (LOG_DEBUG) then luup.log(string.format("Expected checksum: 0x%02x 0x%02x", EXPECTED_CHECKSUM1, EXPECTED_CHECKSUM2)) end
+					if (LOG_DEBUG) then luup.log(string.format("Received checksum: 0x%02x 0x%02x", RECEIVED_CHECKSUM1, b)) end
+					RECEIVE_STATE = INCOMING_EXPECTING_START
+					return 2,
+						string.format("%c%c", EXPECTED_CHECKSUM1, EXPECTED_CHECKSUM2),
+						string.format("%c%c", RECEIVED_CHECKSUM1, b)
+				end
+			end
+			if (LENGTH == 0) then
+				-- Received all bytes.
+				EXPECTED_CHECKSUM1, EXPECTED_CHECKSUM2 = getChecksum()
+				RECEIVE_STATE = INCOMING_EXPECTING_CHECKSUM1
+			end
+		end
+		return 5
+	end
+end
+
+-- handleReadByteResult(state, a, b, c)
+-- We've just read a byte from the alarm system.
+-- - If the message is complete and the checksum is correct, handle it.
+-- - If there was an error, react.
+-- - If the message isn't over yet, keep going.
+function handleReadByteResult(deviceId, state, a, b, c)
+	if (PERMANENT_HANDLERS == nil) then
+		if (LOG_DEBUG) then luup.log("Global handlers not set up yet") end
+		return
+	end
+	if (state == 4) then
+		-- Message complete.  Dispatch it.
+		local handlers = {}
+		for k, v in pairs(PERMANENT_HANDLERS) do
+			handlers[k] = v
+		end
+		for k, v in pairs(TEMPORARY_HANDLERS) do
+			handlers[k] = v
+		end
+		local messageResult = handleMessage(deviceId, handlers, a, b, c)
+		-- May have queued a message to send.
+		processSendQueue()
+		return messageResult
+	elseif (state == 2) then
+		-- Bad checksum.  Send a negative acknowledgment message.
+		return sendNegativeAcknowledgeMessage()
+	elseif (state == 3) then
+		-- Junk byte.  Do nothing.  We will get the message again.
+	else
+		-- state == 5
+		-- Nothing to do yet.
+	end
+end
+
+-- processSendQueue()
+-- If there are pending messages to send to the interface,
+-- send one now.
+function processSendQueue()
+	TEMPORARY_HANDLERS = {}
+	if (RECEIVE_STATE == INCOMING_EXPECTING_START) then
+		local job = getPendingJob()
+		if (job ~= nil) then
+			-- There is a request still to send to the alarm interface.
+			local message = JOBS[job]["message"]
+			TEMPORARY_HANDLERS = JOBS[job]["handlers"]
+			-- Send the message to the alarm interface.
+			sendMessage(message)
+		end
+	end
+end
+
+-- addPendingJob(job, message, handlers)
+-- Log a message that we'd like to send to the interface.
+-- It may not be convenient to send now, so add it to a queue.
+function addPendingJob(job, message, handlers)
+	JOBS[job] = {
+		["complete"] = false,
+		["message"] = message,
+		["handlers"] = handlers
+	}
+	JOBS_PENDING_SEND_TAIL = JOBS_PENDING_SEND_TAIL + 1
+	JOBS_PENDING_SEND[JOBS_PENDING_SEND_TAIL] = job
+end
+
+-- getPendingJob()
+-- Serve the head of the job queue, ready to send its message to the
+-- interface.
+function getPendingJob()
+	if (JOBS_PENDING_SEND_TAIL &gt;= JOBS_PENDING_SEND_HEAD) then
+		local job = JOBS_PENDING_SEND[JOBS_PENDING_SEND_HEAD]
+		-- Remove the job from the queue.
+		JOBS_PENDING_SEND[JOBS_PENDING_SEND_HEAD] = nil
+		JOBS_PENDING_SEND_HEAD = JOBS_PENDING_SEND_HEAD + 1
+		return job
+	else
+		return nil
+	end
+end
+
+-- getJobId(lul_job)
+-- Get a unique string for this job.
+function getJobId(lul_job)
+	return tostring(lul_job)
+end
+
+-- checkJobComplete(lul_job)
+-- Make up the return status for the current job/incoming event.
+-- Check the state of the job and return failure, success, or
+-- put off the decision.
+function checkJobComplete(lul_job)
+	-- if (LOG_DEBUG) then luup.log("Incoming: " .. getJobId(lul_job)) end
+	if (JOBS[getJobId(lul_job)]["complete"]) then
+		-- if (LOG_DEBUG) then luup.log("Incoming: state " .. JOBS[getJobId(lul_job)]["complete"]) end
+		-- This just doesn't work as advertised.
+		-- Returning 4 doesn't stop my job from being called again later.
+		return JOBS[getJobId(lul_job)]["complete"], nil, false
+	end
+	-- Job not complete yet.
+	return 5, 1, false
+end
+
+-- validatePin(p)
+-- Check that a PIN is the right length,
+-- and return a packed three-byte string containing the PIN as nibbles.
+function validatePin(p)
+	if (string.len(p) ~= 4 and CONFIGURATION_PIN_LENGTH == 4) then
+		return nil
+	elseif (string.len(p) ~= 6 and CONFIGURATION_PIN_LENGTH == 6) then
+		return nul
+	end
+
+	local bytes = ""
+	for c = 1,string.len(p),2 do
+		bytes = bytes .. string.char(tonumber(string.sub(p, c, c)) + tonumber(string.sub(p, c + 1, c + 1)) * 16)
+	end
+
+	-- Packed string must be three bytes long.
+	if (CONFIGURATION_PIN_LENGTH == 4) then
+		bytes = bytes .. "\000"
+	end
+		
+	return bytes
+end
+	</functions>
+
+	<incoming>
+		<lua>
+			-- Read one byte, process it.
+			-- luup.log(string.format("Byte incoming: 0x%02x", string.byte(lul_data)))
+			handleReadByteResult(lul_device, readByte(lul_data));
+		</lua>
+	</incoming>
+
+	<startup>caddxInitialize</startup>
+
+	<actionList>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:SecuritySensor1</serviceId>
+			<name>SetArmed</name>
+			<job>
+				if (LOG_DEBUG) then luup.log("Job: Zone: SetArmed " .. lul_device .. " " .. lul_settings.newArmedValue .. " job " .. getJobId(lul_job)) end
+				
+				if (not CAPABILITY_ZONE_BYPASS) then
+					-- Cannot bypass; return error.
+					return 2, nil
+				end
+				local zone = tonumber(string.match(luup.devices[lul_device].id, "%d+"))
+				if (ZONE_STATUS[zone]["isBypassed"] == (lul_settings.newArmedValue == "0")) then
+					-- Already the correct state, nothing to do.
+					if (LOG_DEBUG) then luup.log("Job: already in that state.") end
+					return 4, nil
+				end
+
+				if (LOG_DEBUG) then luup.log("Job: Adding job") end
+				addPendingJob(getJobId(lul_job),
+					"\063" .. string.char(zone-1),
+					{
+						[29] = function(deviceId, message)
+							-- Nothing more to do.  The interface will
+							-- send a snapshot with the updated info RSN,
+							-- and we'll update the zone device then.
+							JOBS[getJobId(lul_job)]["complete"] = 4
+							return 0
+						end,
+						[31] = function(deviceId, message)
+							JOBS[getJobId(lul_job)]["complete"] = 2
+							return 0
+						end,
+					}
+				)
+				if (LOG_DEBUG) then luup.log("Job: Processing send queue") end
+				processSendQueue()
+				if (LOG_DEBUG) then luup.log("Job: Done") end
+				-- Either the timeout or incoming task will be called next.
+				return 5, 10
+			</job>
+			<incoming>
+				checkJobComplete(lul_job)
+			</incoming>
+			<timeout>
+				checkJobComplete(lul_job)
+			</timeout>
+		</action>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:AlarmPartition1</serviceId>
+			<name>RequestQuickArm</name>
+			<job>
+				if (LOG_DEBUG) then luup.log("Job: Partition: RequestQuickArm " .. lul_device .. " job " .. getJobId(lul_job)) end
+				
+				if (not CAPABILITY_SECONDARY_KEYPAD) then
+					-- Cannot quick arm; return error.
+					return 2, nil
+				end
+				local partition = tonumber(string.match(luup.devices[lul_device].id, "%d+"))
+
+				if (LOG_DEBUG) then luup.log("Job: Adding job") end
+				addPendingJob(getJobId(lul_job),
+					"\062\002" .. string.char(partition-1),
+					{
+						[29] = function(deviceId, message)
+							-- Nothing more to do.  The interface will
+							-- send a snapshot with the updated info RSN,
+							-- and we'll update the partition device then.
+							JOBS[getJobId(lul_job)]["complete"] = 4
+							return 0
+						end,
+						[31] = function(deviceId, message)
+							JOBS[getJobId(lul_job)]["complete"] = 2
+							return 0
+						end,
+					}
+				)
+				if (LOG_DEBUG) then luup.log("Job: Processing send queue") end
+				processSendQueue()
+				if (LOG_DEBUG) then luup.log("Job: Done") end
+				-- Either the timeout or incoming task will be called next.
+				return 5, 10
+			</job>
+			<incoming>
+				checkJobComplete(lul_job)
+			</incoming>
+			<timeout>
+				checkJobComplete(lul_job)
+			</timeout>
+		</action>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:AlarmPartition1</serviceId>
+			<name>RequestArm</name>
+			<job>
+				if (LOG_DEBUG) then luup.log("Job: Partition: RequestArm " .. lul_device .. " job " .. getJobId(lul_job)) end
+				
+				if (not CAPABILITY_PRIMARY_KEYPAD_WITH_PIN) then
+					-- Cannot arm; return error.
+					return 2, nil
+				end
+				local partition = tonumber(string.match(luup.devices[lul_device].id, "%d+"))
+				local pinCode = validatePin(lul_settings.PINCode)
+				if (pinCode == nil) then return 2, nil end
+
+				if (LOG_DEBUG) then luup.log("Job: Adding job") end
+				addPendingJob(getJobId(lul_job),
+					"\060" .. pinCode .. "\002" .. string.char(2 ^ (partition-1)),
+					{
+						[29] = function(deviceId, message)
+							-- Nothing more to do.  The interface will
+							-- send a snapshot with the updated info RSN,
+							-- and we'll update the partition device then.
+							JOBS[getJobId(lul_job)]["complete"] = 4
+							return 0
+						end,
+						[31] = function(deviceId, message)
+							JOBS[getJobId(lul_job)]["complete"] = 2
+							return 0
+						end,
+					}
+				)
+				if (LOG_DEBUG) then luup.log("Job: Processing send queue") end
+				processSendQueue()
+				if (LOG_DEBUG) then luup.log("Job: Done") end
+				-- Either the timeout or incoming task will be called next.
+				return 5, 10
+			</job>
+			<incoming>
+				checkJobComplete(lul_job)
+			</incoming>
+			<timeout>
+				checkJobComplete(lul_job)
+			</timeout>
+		</action>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:AlarmPartition1</serviceId>
+			<name>RequestQuickStay</name>
+			<job>
+				if (LOG_DEBUG) then luup.log("Job: Partition: RequestQuickStay " .. lul_device .. " job " .. getJobId(lul_job)) end
+				
+				if (not CAPABILITY_SECONDARY_KEYPAD) then
+					-- Cannot quick stay; return error.
+					return 2, nil
+				end
+				local partition = tonumber(string.match(luup.devices[lul_device].id, "%d+"))
+
+				if (LOG_DEBUG) then luup.log("Job: Adding job") end
+				addPendingJob(getJobId(lul_job),
+					"\062\000" .. string.char(partition-1),
+					{
+						[29] = function(deviceId, message)
+							-- Nothing more to do.  The interface will
+							-- send a snapshot with the updated info RSN,
+							-- and we'll update the partition device then.
+							JOBS[getJobId(lul_job)]["complete"] = 4
+							return 0
+						end,
+						[31] = function(deviceId, message)
+							JOBS[getJobId(lul_job)]["complete"] = 2
+							return 0
+						end,
+					}
+				)
+				if (LOG_DEBUG) then luup.log("Job: Processing send queue") end
+				processSendQueue()
+				if (LOG_DEBUG) then luup.log("Job: Done") end
+				-- Either the timeout or incoming task will be called next.
+				return 5, 10
+			</job>
+			<incoming>
+				checkJobComplete(lul_job)
+			</incoming>
+			<timeout>
+				checkJobComplete(lul_job)
+			</timeout>
+		</action>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:AlarmPartition1</serviceId>
+			<name>RequestStay</name>
+			<job>
+				if (LOG_DEBUG) then luup.log("Job: Partition: RequestStay " .. lul_device .. " job " .. getJobId(lul_job)) end
+				
+				if (not CAPABILITY_PRIMARY_KEYPAD_WITH_PIN) then
+					-- Cannot stay; return error.
+					return 2, nil
+				end
+				local partition = tonumber(string.match(luup.devices[lul_device].id, "%d+"))
+				local pinCode = validatePin(lul_settings.PINCode)
+				if (pinCode == nil) then return 2, nil end
+
+				if (LOG_DEBUG) then luup.log("Job: Adding job") end
+				addPendingJob(getJobId(lul_job),
+					"\060" .. pinCode .. "\003" .. string.char(2 ^ (partition-1)),
+					{
+						[29] = function(deviceId, message)
+							-- Nothing more to do.  The interface will
+							-- send a snapshot with the updated info RSN,
+							-- and we'll update the partition device then.
+							JOBS[getJobId(lul_job)]["complete"] = 4
+							return 0
+						end,
+						[31] = function(deviceId, message)
+							JOBS[getJobId(lul_job)]["complete"] = 2
+							return 0
+						end,
+					}
+				)
+				if (LOG_DEBUG) then luup.log("Job: Processing send queue") end
+				processSendQueue()
+				if (LOG_DEBUG) then luup.log("Job: Done") end
+				-- Either the timeout or incoming task will be called next.
+				return 5, 10
+			</job>
+			<incoming>
+				checkJobComplete(lul_job)
+			</incoming>
+			<timeout>
+				checkJobComplete(lul_job)
+			</timeout>
+		</action>
+		<action>
+			<serviceId>urn:micasaverde-com:serviceId:AlarmPartition1</serviceId>
+			<name>RequestDisarm</name>
+			<job>
+				if (LOG_DEBUG) then luup.log("Job: Partition: RequestDisarm " .. lul_device .. " job " .. getJobId(lul_job)) end
+				
+				if (not CAPABILITY_PRIMARY_KEYPAD_WITH_PIN) then
+					-- Cannot disarm; return error.
+					return 2, nil
+				end
+				local partition = tonumber(string.match(luup.devices[lul_device].id, "%d+"))
+				local pinCode = validatePin(lul_settings.PINCode)
+				if (pinCode == nil) then return 2, nil end
+
+				if (LOG_DEBUG) then luup.log("Job: Adding job") end
+				addPendingJob(getJobId(lul_job),
+					"\060" .. pinCode .. "\001" .. string.char(2 ^ (partition-1)),
+					{
+						[29] = function(deviceId, message)
+							-- Nothing more to do.  The interface will
+							-- send a snapshot with the updated info RSN,
+							-- and we'll update the partition device then.
+							JOBS[getJobId(lul_job)]["complete"] = 4
+							return 0
+						end,
+						[31] = function(deviceId, message)
+							JOBS[getJobId(lul_job)]["complete"] = 2
+							return 0
+						end,
+					}
+				)
+				if (LOG_DEBUG) then luup.log("Job: Processing send queue") end
+				processSendQueue()
+				if (LOG_DEBUG) then luup.log("Job: Done") end
+				-- Either the timeout or incoming task will be called next.
+				return 5, 10
+			</job>
+			<incoming>
+				checkJobComplete(lul_job)
+			</incoming>
+			<timeout>
+				checkJobComplete(lul_job)
+			</timeout>
+		</action>
+	</actionList>
+</implementation>
Index: /trunk/vera-caddx/S_AlarmPartition1.xml
===================================================================
--- /trunk/vera-caddx/S_AlarmPartition1.xml	(revision 3)
+++ /trunk/vera-caddx/S_AlarmPartition1.xml	(revision 3)
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<scpd xmlns="urn:schemas-upnp-org:service-1-0">
+  <specVersion>
+    <major>1</major>
+    <minor>0</minor>
+  </specVersion>
+  <serviceStateTable>
+    <stateVariable sendEvents="yes">
+      <name>Disarmed</name>
+      <dataType>boolean</dataType>
+      <defaultValue>true</defaultValue>
+    </stateVariable>
+    <stateVariable sendEvents="yes">
+      <name>Armed</name>
+      <dataType>boolean</dataType>
+      <defaultValue>false</defaultValue>
+    </stateVariable>
+    <stateVariable sendEvents="yes">
+      <name>StayArmed</name>
+      <dataType>boolean</dataType>
+      <defaultValue>false</defaultValue>
+    </stateVariable>
+    <stateVariable sendEvents="yes">
+      <name>Breach</name>
+      <dataType>boolean</dataType>
+      <defaultValue>false</defaultValue>
+    </stateVariable>
+  </serviceStateTable>
+  <actionList>
+    <action>
+      <name>RequestArm</name>
+      <argumentList>
+        <argument>
+          <name>PINCode</name>
+          <direction>in</direction>
+        </argument>
+      </argumentList>
+    </action>
+    <action>
+      <name>RequestQuickArm</name>
+      <argumentList>
+      </argumentList>
+    </action>
+    <action>
+      <name>RequestStay</name>
+      <argumentList>
+        <argument>
+          <name>PINCode</name>
+          <direction>in</direction>
+        </argument>
+      </argumentList>
+    </action>
+    <action>
+      <name>RequestQuickStay</name>
+      <argumentList>
+      </argumentList>
+    </action>
+    <action>
+      <name>RequestDisarm</name>
+      <argumentList>
+        <argument>
+          <name>PINCode</name>
+          <direction>in</direction>
+        </argument>
+      </argumentList>
+    </action>
+    <action>
+      <name>SetVirtualInput</name>
+      <argumentList>
+        <argument>
+          <name>Input</name>
+          <direction>in</direction>
+        </argument>
+        <argument>
+          <name>NewState</name>
+          <direction>in</direction>
+        </argument>
+      </argumentList>
+    </action>
+  </actionList>
+</scpd>
Index: /trunk/vera-caddx/D_CaddxNX584Security.xml
===================================================================
--- /trunk/vera-caddx/D_CaddxNX584Security.xml	(revision 3)
+++ /trunk/vera-caddx/D_CaddxNX584Security.xml	(revision 3)
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<root xmlns="urn:schemas-upnp-org:device-1-0">
+  <specVersion>
+    <major>1</major>
+    <minor>0</minor>
+  </specVersion>
+  <device>
+    <deviceType>urn:schemas-futzle-com:device:caddx-alarm:1</deviceType>
+    <friendlyName>Caddx NX584 Security System</friendlyName>
+    <manufacturer>GE Network</manufacturer>
+    <manufacturerURL></manufacturerURL>
+    <modelDescription>Caddx NX584 Security System</modelDescription>
+    <modelName>NX584</modelName>
+    <modelNumber>1.0</modelNumber>
+    <modelURL></modelURL>
+    <protocol>raw</protocol>
+    <handleChildren>1</handleChildren>
+    <UDN>uuid:78f5ed30-db5e-46c7-a12e-838c9d4eb0e3</UDN>
+    <UPC>123456789</UPC>
+    <serviceList>
+    </serviceList>
+    <implementationList>
+      <implementationFile>I_CaddxNX584Security.xml</implementationFile>
+    </implementationList>
+    <presentationURL></presentationURL>
+  </device>
+</root>
Index: /trunk/D_AlarmPartition1.xml
===================================================================
--- /trunk/D_AlarmPartition1.xml	(revision 3)
+++ /trunk/D_AlarmPartition1.xml	(revision 3)
@@ -0,0 +1,149 @@
+<?xml version="1.0"?>
+<root xmlns="urn:schemas-upnp-org:device-1-0">
+  <specVersion>
+    <major>1</major>
+    <minor>0</minor>
+  </specVersion>
+  <device>
+    <deviceType>urn:schemas-micasaverde-com:device:AlarmPartition:1</deviceType>
+	<staticJson>D_AlarmPartition1.json</staticJson> 
+    <serviceList>
+      <service>
+        <serviceType>urn:schemas-micasaverde-com:service:AlarmPartition:1</serviceType>
+        <serviceId>urn:micasaverde-com:serviceId:AlarmPartition1</serviceId>
+        <controlURL>/upnp/control/AlarmPartition1</controlURL>
+        <eventSubURL>/upnp/event/AlarmPartition1</eventSubURL>
+        <SCPDURL>S_AlarmPartition1.xml</SCPDURL>
+      </service>
+    </serviceList>
+    <eventList>
+{
+    "event_1":{
+        "label": "Partition is Armed",
+        "serviceId": "urn:micasaverde-com:serviceId:AlarmPartition1",
+        "argumentList" :
+        {
+            "argument_1" :
+            {
+                "dataType": "boolean",
+                "defaultValue": "",
+                "allowedValueList" : 
+                {
+                    "Yes" : "1",
+                    "No" : "0"
+                },
+                "name": "Armed",
+                "comparisson": "=",
+                "prefix": "Is Armed?",
+                "suffix": ""
+            }
+        }
+    },
+    "event_2":{
+        "label": "Partition is Stay-Armed",
+        "serviceId": "urn:micasaverde-com:serviceId:AlarmPartition1",
+        "argumentList" :
+        {
+            "argument_1" :
+            {
+                "dataType": "boolean",
+                "defaultValue": "",
+                "allowedValueList" : 
+                {
+                    "Yes" : "1",
+                    "No" : "0"
+                },
+                "name": "StayArmed",
+                "comparisson": "=",
+                "prefix": "Is Stay Armed?",
+                "suffix": ""
+            }
+        }
+    },
+    "event_3":{
+        "label": "Partition is Breached",
+        "serviceId": "urn:micasaverde-com:serviceId:AlarmPartition1",
+        "argumentList" :
+        {
+            "argument_1" :
+            {
+                "dataType": "boolean",
+                "defaultValue": "",
+                "allowedValueList" : 
+                {
+                    "Yes" : "1",
+                    "No" : "0"
+                },
+                "name": "Breach",
+                "comparisson": "=",
+                "prefix": "In Breach?",
+                "suffix": ""
+            }
+        }
+    },
+    "event_4":{
+        "label": "Partition is Disarmed",
+        "serviceId": "urn:micasaverde-com:serviceId:AlarmPartition1",
+        "argumentList" :
+        {
+            "argument_1" :
+            {
+                "dataType": "boolean",
+                "defaultValue": "",
+                "allowedValueList" : 
+                {
+                    "Yes" : "1",
+                    "No" : "0"
+                },
+                "name": "Disarmed",
+                "comparisson": "=",
+                "prefix": "Is Disarmed?",
+                "suffix": ""
+            }
+        }
+    }
+}
+    </eventList>	
+    <sceneList>
+{
+    "group_1":{
+        "cmd_1":
+        {
+            "label": "Set Virtual Input Open/Closed",
+            "serviceId": "urn:micasaverde-com:serviceId:AlarmPartition1",
+            "action": "SetVirtualInput",
+            "argumentList":
+            {
+                "argument_1":
+                {
+                    "dataType": "ui1",
+                    "defaultValue": "1",
+                    "allowedValueRange":
+                    {
+                        "minimum": "1",
+                        "maximum": "999"
+                    },
+                    "prefix" : "Input#",
+                    "suffix" : "(1-999)",
+                    "name": "Input"
+                },
+                "argument_2":
+                {
+                    "dataType": "ui1",
+                    "defaultValue": "1",
+                    "allowedValueList":
+                    {
+                        "Open": "0",
+                        "Closed": "1"
+                    },
+                    "prefix" : "",
+                    "suffix" : "",
+                    "name": "NewState"
+                },
+            }
+        }
+    }
+}
+    </sceneList>
+  </device>
+</root>
