Index: /S_UPnPProxy1.xml
===================================================================
--- /S_UPnPProxy1.xml	(revision 3)
+++ /S_UPnPProxy1.xml	(revision 3)
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<scpd xmlns="urn:schemas-upnp-org:service-1-0">
+  <specVersion>
+    <major>1</major>
+    <minor>0</minor>
+  </specVersion>
+  <serviceStateTable>
+    <stateVariable allowRepeats="no">
+      <name>Status</name>
+      <dataType>integer</dataType>
+    </stateVariable>
+  </serviceStateTable>
+  <actionList>
+    <action>
+      <name>uninstall</name>
+      <argumentList>
+      </argumentList>
+    </action>
+  </actionList>
+</scpd>
Index: /D_UPnPProxy1.xml
===================================================================
--- /D_UPnPProxy1.xml	(revision 3)
+++ /D_UPnPProxy1.xml	(revision 3)
@@ -0,0 +1,40 @@
+<?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:UPnPProxy:1</deviceType>
+    <staticJson>D_UPnPProxy1.json</staticJson>
+    <friendlyName>UPnP Event Proxy</friendlyName>
+    <manufacturer>Futzle Industries</manufacturer>
+    <manufacturerURL></manufacturerURL>
+    <modelDescription></modelDescription>
+    <modelName></modelName>
+    <modelNumber>1.0</modelNumber>
+    <modelURL></modelURL>
+    <UDN></UDN>
+    <UPC>123456789</UPC>
+    <serviceList>
+      <service>
+        <serviceType>urn:schemas-futzle-com:service:UPnPProxy:1</serviceType>
+        <serviceId>urn:futzle-com:serviceId:UPnPProxy1</serviceId>
+        <controlURL>/upnp/control/UPnPProxy1</controlURL>
+        <eventSubURL>/upnp/event/UPnPProxy1</eventSubURL>
+        <SCPDURL>S_UPnPProxy1.xml</SCPDURL>
+      </service>
+      <service>
+        <serviceType>urn:schemas-micasaverde-com:service:HaDevice:1</serviceType>
+        <serviceId>urn:micasaverde-com:serviceId:HaDevice1</serviceId>
+        <controlURL>/upnp/control/HaDevice1</controlURL>
+        <eventSubURL>/upnp/event/HaDevice1</eventSubURL>
+        <SCPDURL>S_HaDevice1.xml</SCPDURL>
+      </service>
+    </serviceList>
+    <implementationList>
+      <implementationFile>I_UPnPProxy1.xml</implementationFile>
+    </implementationList>
+    <presentationURL></presentationURL>
+  </device>
+</root>
Index: /L_UPnPProxyDaemon.lua
===================================================================
--- /L_UPnPProxyDaemon.lua	(revision 3)
+++ /L_UPnPProxyDaemon.lua	(revision 3)
@@ -0,0 +1,507 @@
+#!/usr/bin/lua
+--[[
+
+	UPnP event proxy
+	
+	Copyright 2013 Deborah Pickett
+
+	Parts of this program have been modified from the Luasocket library
+	(http://w3.impa.br/~diego/software/luasocket/), released under the MIT licence.
+
+	Version 0.1 2013-03-17 API version 1
+	Version 0.1 2013-04-07 API version 2 (locking)
+	Version 0.1 2013-04-09 API version 3 (locking replaced with random retries)
+
+	This program listens on a TCP port (2529) for a subset of HTTP requests.
+	
+	GET /version HTTP/1.1
+	Returns 200 OK, and the current API version of this program.
+
+	PUT /upnp/event/[SID] HTTP/1.1
+	Request that the proxy send notifications for the subscription [SID]
+	to a nominated MiOS device.  Notifications are sent when a variable's
+	value is sent by the UPnP device.
+		<subscription expiry="timestamp">
+			<variable name="UPnPVariableName" host="localhost" deviceId="123"
+				serviceId="serviceWithAction" action="deviceActionName"
+				parameter="deviceActionParameter"
+				sidParameter="deviceActionParameter"
+			/>
+			<!-- more <variable> entries here. -->
+		</subscription>
+		Parameters:
+			expiry: os.time() before which renewal must happen.
+			name: the UPnP variable that the device is interested in.
+			host: the address to forward the event to.  Defaults to localhost.
+			deviceId: the device number that wants to be informed of the event.
+			serviceId: the service Id of the action to invoke.
+			action: the name of the action to invoke on the target device.
+			parameter: the name of the parameter that will contain the value of the variable
+			sidParameter: the name of the parameter that will contain the SID.
+
+	DELETE /upnp/event/[SID] HTTP/1.1
+	Request that this program no longer forwards events for subscription [SID].
+
+	NOTIFY /upnp/event HTTP/1.1
+	Sent by the UPnP device.  Format is as specified in the UPnP spec.
+	Events that are subscribed to will be forwarded to the previously registered
+	device with the registered action.
+]]
+
+local socket = require('socket')
+local url = require('socket.url')
+local http = require('socket.http')
+local ltn12 = require('ltn12')
+local lxp = require('lxp')
+
+local API_VERSION = "3"
+local LISTEN_PORT = 2529
+local LISTEN_BACKLOG = 5
+local LISTEN_TIMEOUT = 10
+
+local subscriptions = {}
+local notificationQueue = {}
+
+function log(s)
+	print(s)
+end
+
+-- Parse the HTTP request line.
+-- Returns the method and the path (split at / characters)
+function readStatusLine(c)
+	local statusLine = c:receive('*l')
+	local method, path = statusLine:match("^(%u+) ([^ ]+) HTTP/1.[01]")
+	if (statusLine:match("^GET ")) then
+		return "GET", url.parse_path(path)
+	elseif (statusLine:match("^PUT ")) then
+		return "PUT", url.parse_path(path)
+	elseif (statusLine:match("^NOTIFY ")) then
+		return "NOTIFY", url.parse_path(path)
+	elseif (statusLine:match("^DELETE ")) then
+		return "DELETE", url.parse_path(path)
+	else
+		return nil, "405 Method not allowed"
+	end
+end
+
+-- Parse the HTTP headers.
+-- Returns them as a table.
+function readHeaders(c)
+	local headers = {}
+	local headerLine = c:receive('*l')
+	while (headerLine) do
+		local nextLine = c:receive('*l')
+		if (not nextLine) then
+			-- Error, should always be a blank line at least.	
+			return nil
+		end
+		if (nextLine:match("^%s")) then
+			headerLine = headerLine .. nextLine:gsub("%s+", " ", 1)
+		else
+			-- Header line does not start with space; previous header is complete.
+			local name, value = headerLine:match("^([^:]+): ?(.*)$")
+			-- log("header: " .. name)
+			-- log("value: " .. value)
+			headers[name:lower()] = value
+			if (nextLine == "") then
+				return headers
+			end
+			headerLine = nextLine
+		end
+	end
+end
+
+-- Fetch the HTTP body.
+function readBody(c, len)
+	local result = ""
+	if (len) then
+		-- log("Reading " .. len .. " bytes")
+		local bytesToRead = len
+		while (bytesToRead > 0) do
+			local data, reason = c:receive(len)
+			if (data) then
+				bytesToRead = bytesToRead - data:len()
+				result = result .. data
+			else
+				return nil, reason
+			end
+		end
+	else
+		-- Untested!
+		log("Reading till EOF")
+		while (true) do
+			local data, reason = c:receive("*a")
+			log(data)
+			log(reason)
+			if (data) then
+				result = result .. data
+			else
+				return nil, reason
+			end
+		end
+	end
+
+	return result
+end
+
+-- Add a notification to the notification queue.
+-- This queue will be sent when the proxy process
+-- is back in its main loop.
+function queueVariableNotification(sid, variable, value)
+	log("Queueing notification for " .. sid .. "/" .. variable.name .. " = " .. value)
+	table.insert(notificationQueue, {
+		delayUntil = os.time(),
+		retries = 3,
+		url = "http://" .. variable.host .. ":3480/data_request?id=lu_action" ..
+		"&DeviceNum=" .. url.escape(variable.deviceId) ..
+		"&serviceId=" .. url.escape(variable.serviceId) ..
+		"&action=" .. url.escape(variable.action) ..
+		"&" .. url.escape(variable.parameter) .. "=" .. url.escape(value) ..
+		(variable.sidParameter and ("&" .. url.escape(variable.sidParameter) .. "=" .. url.escape(sid)) or "")
+	})
+end
+
+-- Ensure that a subscription is known about.
+function createSubscription(sid, expiry, clearProxy)
+	if (not subscriptions[sid]) then
+		-- Unknown subscription.  Keep it around in case someone asks.
+		if (not expiry) then
+			-- 60 seconds ought to be enough.
+			expiry = os.time() + 60
+		end
+		subscriptions[sid] = {
+			variables = {},
+			proxy = {},
+			expiry = expiry,
+		}
+	end
+	if (clearProxy) then
+		subscriptions[sid].proxy = {}
+	end
+end
+
+-- Called when a UPnP device sends an event.
+-- Store the value, and decide if a MiOS
+-- plugin is interested in knowing about it.
+function updateSubscribedVariable(sid, name, value)
+	createSubscription(sid)
+	subscriptions[sid].variables[name] = value
+	log("Updated variable in subscription " .. sid .. ": " .. name .. " = " .. value)
+	-- Proxy target may have already checked in.
+	for _, v in pairs(subscriptions[sid].proxy) do
+		if (name == v.name) then
+			queueVariableNotification(sid, v, value)
+		end
+	end
+end
+
+-- Called when a MiOS plugin tells us that it
+-- has requested a subscription.
+-- Store the callback information, and decide if
+-- the device has already reported a value for this
+-- variable.
+function addProxy(sid, variable)
+	createSubscription(sid)
+	table.insert(subscriptions[sid].proxy, variable)
+	if (subscriptions[sid].variables[variable.name]) then
+		-- Variable value is already known.  Queue up a notification.
+		queueVariableNotification(sid, variable, subscriptions[sid].variables[variable.name])
+	end
+end
+
+-- Returns a parser object that handles a UPnP NOTIFY body.
+function createUpnpNotificationParser()
+	local currentXpathTable = {}
+
+	local currentXpath = function()
+		return "/" .. table.concat(currentXpathTable, "/")
+	end
+
+	local result = {}
+	local variableName = nil
+
+	local xmlParser = lxp.new({
+		CharacterData = function(parser, string)
+			if (variableName) then
+				result[variableName] = result[variableName] .. string
+			end
+	 	end,
+		StartElement = function(parser, elementName, attributes)
+			-- log("Start element: " .. elementName)
+			table.insert(currentXpathTable, elementName)
+			if (#currentXpathTable == 3 and currentXpathTable[1] == "urn:schemas-upnp-org:event-1-0|propertyset" and currentXpathTable[2] == "urn:schemas-upnp-org:event-1-0|property") then
+				-- log("Variable: " .. elementName)
+				variableName = elementName
+				result[variableName] = ""
+			end
+		end,
+		EndElement = function(parser, elementName)
+			table.remove(currentXpathTable)
+			variableName = nil
+		end,
+	}, "|")
+
+	return {
+		parse = function(this, s) return xmlParser:parse(s) end,
+		close = function(this) xmlParser:close() end,
+		result = function(this) return result end,
+	}
+end
+
+-- Returns a parser object that handles PUT requests
+-- made by a MiOS plugin wanting to be informed of events.
+function createProxyRequestParser()
+	local currentXpathTable = {}
+
+	local currentXpath = function()
+		return "/" .. table.concat(currentXpathTable, "/")
+	end
+
+	local variables = {}
+	local expiry
+
+	local xmlParser = lxp.new({
+		StartElement = function(parser, elementName, attributes)
+			-- log("Start element: " .. elementName)
+			table.insert(currentXpathTable, elementName)
+			if (#currentXpathTable == 1 and currentXpathTable[1] == "subscription") then
+				expiry = attributes.expiry;
+			elseif (#currentXpathTable == 2 and currentXpathTable[1] == "subscription" and currentXpathTable[2] == "variable") then
+				local variable = {}
+				variable.name = attributes.name
+				variable.host = attributes.host
+				variable.deviceId = attributes.deviceId
+				variable.serviceId = attributes.serviceId
+				variable.action = attributes.action
+				variable.parameter = attributes.parameter
+				variable.sidParameter = attributes.sidParameter
+				table.insert(variables, variable)
+			end
+		end,
+		EndElement = function(parser, elementName)
+			table.remove(currentXpathTable)
+		end,
+	}, "|")
+
+	return {
+		parse = function(this, s) return xmlParser:parse(s) end,
+		close = function(this) xmlParser:close() end,
+		expiry = function(this) return expiry end,
+		variables = function(this) return variables end,
+	}
+end
+
+-- Handle all PUT requests:
+--   MiOS plugin is telling us of a subscription that it made.
+function handlePutRequest(c, path, headers, body)
+	if (#path == 3 and path[1] == "upnp" and path[2] == "event") then
+		-- Plugin is asking to be sent notifications for this subscription.
+		local sid = path[3]
+		local proxyRequestParser = createProxyRequestParser()
+		local result, reason = proxyRequestParser:parse(body)
+		proxyRequestParser:close()
+		if (result) then
+			local variables = proxyRequestParser:variables()
+			local expiry = proxyRequestParser:expiry()
+			createSubscription(sid, tonumber(expiry), true)
+			for i = 1, #variables do
+				log ("Will forward events for " .. variables[i].name)
+				addProxy(sid, variables[i])
+			end
+			c:send("HTTP/1.1 200 OK\r\n\r\n")
+		else
+			log("Parser failed: " .. reason)
+			c:send("HTTP/1.1 412 Precondition Failed\r\n\r\n")
+		end
+	else
+		c:send("HTTP/1.1 403 Forbidden\r\n\r\n")
+	end
+	return true
+end
+
+-- Handle all GET requests:
+--   API version of this web server.
+function handleGetRequest(c, path, headers)
+	if (#path == 1 and path[1] == "version") then
+		-- GET /version
+		-- Returns the API version.
+		c:send("HTTP/1.1 200 OK\r\n" .. "Content-Length: " .. API_VERSION:len() .. "\r\nContent-Type: text/plain\r\n\r\n" .. API_VERSION)
+	else
+	  c:send("HTTP/1.1 404 Not found\r\n\r\n")
+	end
+	return true
+end
+
+-- Handle all DELETE requests:
+--   MiOS plugin doesn't want to be informed about a subscription.
+function handleDeleteRequest(c, path, headers)
+	if (#path == 3 and path[1] == "upnp" and path[2] == "event") then
+		-- Plugin is asking to no longer be sent notifications for this subscription.
+		local sid = path[3]
+		subscriptions[sid] = nil
+		c:send("HTTP/1.1 200 OK\r\n\r\n")
+	else
+		c:send("HTTP/1.1 404 Not Found\r\n\r\n")
+	end
+	return true
+end
+
+-- Handle all NOTIFY requests:
+--   UPnP device is sending an event that a MiOS plugin subscribed to.
+function handleNotifyRequest(c, path, headers, body)
+	if (#path == 2 and path[1] == "upnp" and path[2] == "event") then
+		-- NOTIFY /upnp/event
+		-- UPnP device is informing of a state change.
+		-- UPnP spec deviation: WeMo devices insert NUL bytes in the XML.
+		body = body:gsub("%z", " ")
+		local upnpParser = createUpnpNotificationParser()
+		local result, reason = upnpParser:parse(body)
+		upnpParser:close()
+		if (result) then
+			-- log("Parser succeeded")
+			for k, v in pairs(upnpParser.result()) do
+				updateSubscribedVariable(headers["sid"], k, v)
+			end
+			c:send("HTTP/1.1 200 OK\r\n\r\n")
+		else
+			log("Parser failed: " .. reason)
+			c:send("HTTP/1.1 412 Precondition Failed\r\n\r\n")
+		end
+	else
+		c:send("HTTP/1.1 404 Not Found\r\n\r\n")
+	end
+	return true
+end
+
+-- Handle an incoming HTTP request.
+function handleRequest(c, method, path, headers, body)
+	if (method == "PUT") then
+		return handlePutRequest(c, path, headers, body)
+	elseif (method == "GET") then
+		return handleGetRequest(c, path, headers)
+	elseif (method == "DELETE") then
+		return handleDeleteRequest(c, path, headers)
+	elseif (method == "NOTIFY") then
+		return handleNotifyRequest(c, path, headers, body)
+	else
+		c:send("405 Method Not Allowed\r\n\r\n")
+		return true
+	end
+end
+
+-- Send outstanding notifications to MiOS plugins.
+function processNotificationQueue()
+	local saveForLater = {}
+	local nextTimeout = LISTEN_TIMEOUT
+	while (#notificationQueue > 0) do
+		local notification = table.remove(notificationQueue, 1)
+		if (notification.delayUntil > os.time()) then
+			-- Don't send yet, device may not be ready.
+			log("Event not ready, try again later")
+			table.insert(saveForLater, notification)
+		else
+			log("Sending notification: " .. notification.url)
+			local request, reason = http.request(notification.url)
+			-- Oh god, string matching.  "Device Not Ready" still comes with a 200 OK.
+			if (request and not(request:match("ERROR: Device not ready"))) then
+				-- Successful notification.
+				-- log("Response: " .. request)
+			else
+				log("Error: " .. reason)
+				if (notification.retries > 0) then
+					local randomDelay = math.random(1, 5)
+					notification.delayUntil = os.time() + randomDelay
+					notification.retries = notification.retries - 1
+					nextTimeout = math.min(nextTimeout, randomDelay)
+					table.insert(saveForLater, notification)
+				end
+			end
+			-- Wait at most ten seconds, at least 1.
+			nextTimeout = math.min(nextTimeout, LISTEN_TIMEOUT)
+			nextTimeout = math.max(nextTimeout, 1)
+		end
+	end
+	-- Unsuccessful notifications will be requeued for next time.
+	for _, notification in pairs(saveForLater) do
+		table.insert(notificationQueue, notification)
+	end
+	return nextTimeout
+end
+
+-- Eliminate old subscriptions to prevent memory leaks.
+function purgeExpiredSubscriptions()
+	for sid, info in pairs(subscriptions) do
+		if (info.expiry < os.time()) then
+			subscriptions[sid] = nil
+		end
+	end
+end
+
+-- Run until instructed to exit (not yet implemented)
+local runServer = true
+
+local s = socket.tcp()
+s:setoption("reuseaddr", true)
+-- Every LISTEN_TIMEOUT seconds deal with retried notifications.
+s:settimeout(LISTEN_TIMEOUT)
+local result, reason = s:bind('*', LISTEN_PORT)
+if (not result) then
+	log("Cannot bind to port: " .. reason)
+	os.exit(1)
+end
+result, reason = s:listen(LISTEN_BACKLOG)
+if (not result) then
+	log("Cannot listen: " .. reason)
+	os.exit(1)
+end
+
+repeat
+	-- Wait (until timeout, perhaps) for a connection.
+	local c = s:accept()
+	if (c) then
+		local remoteHost = c:getpeername()
+		-- log("Connection established from " .. remoteHost)
+
+		local method, path = readStatusLine(c)
+		if (method) then
+			local logstring = os.date() .. " " .. remoteHost .. " > " .. method .. " "
+			for i = 1, #path do
+				logstring = logstring .. "/" .. path[i]
+			end
+			log(logstring)
+			-- So far so good.
+			local headers = readHeaders(c)
+			if (headers) then
+				-- Now read body.
+				local body
+				if (method == "GET" or method == "DELETE") then
+					body = nil
+				elseif (headers["content-length"]) then
+					body = readBody(c, tonumber(headers["content-length"]))
+				else
+					body = readBody(c, nil)
+				end
+				-- Dispatch the request.
+				runServer = handleRequest(c, method, path, headers, body)
+			else
+				log("Error while processing headers")
+				c:send("HTTP/1.1 400 Bad Request\r\n\r\n")
+			end
+		else
+			-- path contains the error code.
+			log("Error while processing request line: " .. path)
+			c:send("HTTP/1.1 500 " .. path .. "\r\n\r\n")
+		end
+		c:close()
+
+	end
+
+	-- Send outstanding notifications.
+	s:settimeout(processNotificationQueue())
+
+  purgeExpiredSubscriptions()
+
+until not runServer
+
+s:close()
+
Index: /D_UPnPProxy1.json
===================================================================
--- /D_UPnPProxy1.json	(revision 3)
+++ /D_UPnPProxy1.json	(revision 3)
@@ -0,0 +1,146 @@
+{
+    "flashicon": "icons/generic.swf",
+    "imgIconBody": "",
+    "imgIconDimmable": "",
+    "imgIconTurnable": "",
+    "halloIconsDir": "pics/hallo",
+    "inScene": "0",
+    "DisplayStatus": {},
+    "doc_url": {
+        "doc_language": 1,
+        "doc_manual": 1,
+        "doc_version": 1,
+        "doc_platform": 0,
+        "doc_page": "devices"
+    },
+    "ToggleButton": 1,
+    "Tabs": [
+        {
+            "Label": {
+                "lang_tag": "tabname_control",
+                "text": "Control"
+            },
+            "Position": "0",
+            "TabType": "flash",
+            "SceneGroup": [
+                {
+                    "id": 1,
+                    "top": 1,
+                    "left": 0,
+                    "x": 2,
+                    "y": 2
+                }
+            ],
+            "ControlGroup": [
+                {
+                    "id": "1",
+                    "type": "info",
+                    "scenegroup": 1
+                },
+                {
+                    "id": "2",
+                    "type": "info",
+                    "scenegroup": 2
+                }
+            ],
+            "Control": [
+                {
+                    "ControlGroup": "1",
+                    "ControlHeader": "1",
+                    "ControlType": "label",
+                    "top": 0,
+                    "left": 0,
+                    "text_align": "left",
+                    "Label": {
+                        "lang_tag": "status",
+                        "text": "Status:"
+                    },
+                    "Display": {
+                        "Top": 10,
+                        "Left": 10,
+                        "Width": 50,
+                        "Height": 20
+                    }
+                },
+                {
+                    "ControlGroup": "1",
+                    "ControlHeader": "1",
+                    "ControlType": "variable",
+                    "top": 0,
+                    "left": 0.8,
+                    "text_align": "left",
+                    "x": 1.2,
+                    "Display": {
+                        "Top": 10,
+                        "Left": 80,
+                        "Width": 150,
+                        "Height": 20,
+                        "Service": "urn:futzle-com:serviceId:UPnPProxy1",
+                        "Variable": "StatusText"
+                    }
+                },
+                {
+                    "ControlGroup": "2",
+                    "ControlType": "button",
+                    "Label": {
+                        "lang_tag": "cmd_uninstall",
+                        "text": "Uninstall"
+                    },
+                    "Display": {
+                        "Top": 50,
+                        "Left": 10,
+                        "Width": 100,
+                        "Height": 20
+                    },
+                    "Command": {
+                        "Service": "urn:futzle-com:serviceId:UPnPProxy1",
+                        "Action": "uninstall",
+                        "Parameters": []
+                    }
+                }
+            ]
+        },
+        {
+            "Label": {
+                "lang_tag": "advanced",
+                "text": "Advanced"
+            },
+            "Position": "6",
+            "TabType": "javascript",
+            "ScriptName": "shared.js",
+            "Function": "advanced_device"
+        },
+        {
+            "Label": {
+                "lang_tag": "notifications",
+                "text": "Notifications"
+            },
+            "Position": "7",
+            "TabType": "javascript",
+            "ScriptName": "shared.js",
+            "Function": "device_notifications"
+        },
+        {
+            "Label": {
+                "lang_tag": "logs",
+                "text": "Logs"
+            },
+            "Position": "8",
+            "TabType": "javascript",
+            "ScriptName": "shared.js",
+            "Function": "device_logs"
+        }
+    ],
+    "DeviceType": "urn:schemas-futzle-com:device:UPnPProxy:1",
+    "eventList2": [],
+    "sceneList": {
+        "group_1": {
+            "cmd_1": {
+                "label": "Uninstall",
+                "serviceId": "urn:futzle-com:serviceId:UPnPProxy1",
+                "action": "uninstall",
+                "argumentList": {}
+            }
+        }
+    }
+}
Index: /I_UPnPProxy1.xml
===================================================================
--- /I_UPnPProxy1.xml	(revision 3)
+++ /I_UPnPProxy1.xml	(revision 3)
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<implementation>
+	<functions>
+local plugin
+
+function initialize(lul_device)
+	-- Put the /etc/cmh-ludl and /etc/cmh-lu directories into the Lua include path.
+	if (package.path:find("/etc/cmh-ludl/?.lua;/etc/cmh-lu/?.lua", 1, true) == nil) then
+		package.path = package.path .. ";/etc/cmh-ludl/?.lua;/etc/cmh-lu/?.lua"
+	end
+	-- Load the Lua implementation.
+	package.loaded.L_UPnPProxy1 = nil
+	plugin = require("L_UPnPProxy1")
+	restartNeeded = plugin.restartNeeded
+	updateProxyVersion = plugin.updateProxyVersion
+	-- Call initialization code.
+	return plugin.initialize(lul_device)
+end
+	</functions>
+
+	<startup>initialize</startup>
+
+	<actionList>
+		<action>
+			<serviceId>urn:futzle-com:serviceId:UPnPProxy1</serviceId>
+			<name>uninstall</name>
+			<run>
+				return plugin.uninstall(lul_device)
+			</run>
+		</action>
+	</actionList>
+</implementation>
Index: /L_UPnPProxy1.lua
===================================================================
--- /L_UPnPProxy1.lua	(revision 3)
+++ /L_UPnPProxy1.lua	(revision 3)
@@ -0,0 +1,178 @@
+--
+-- WeMo plugin
+-- Copyright (C) 2013 Deborah Pickett
+-- 
+-- Version 0.0 2013-04-14 by Deborah Pickett
+--
+
+module ("L_UPnPProxy1", package.seeall)
+
+local http = require("socket.http")
+local ltn12 = require("ltn12")
+
+local initScript = [=[#!/bin/sh /etc/rc.common
+# Copyright (C) 2007 OpenWrt.org
+START=80
+PID_FILE=/var/run/upnp-event-proxy.pid
+PROXY_DAEMON=/tmp/upnp-event-proxy.lua
+start() {
+	if [ -f "$PID_FILE" ]; then
+		# May already be running.
+		PID=$(cat "$PID_FILE")
+		if [ -d "/proc/$PID" ]; then
+			COMMAND=$(readlink "/proc/$PID/exe")
+			if [ "$COMMAND" = "/usr/bin/lua" ]; then
+				echo "Daemon is already running"
+				return 1
+			fi
+		fi
+	fi
+	# Find and decompress the proxy daemon Lua source.
+	if [ -f /etc/cmh-ludl/L_UPnPProxyDaemon.lua.lzo ]; then
+		PROXY_DAEMON_LZO=/etc/cmh-ludl/L_UPnPProxyDaemon.lua.lzo
+	elif [ -f /etc/cmh-lu/L_UPnPProxyDaemon.lua.lzo ]; then
+		PROXY_DAEMON_LZO=/etc/cmh-lu/L_UPnPProxyDaemon.lua.lzo
+	fi
+	if [ -n "$PROXY_DAEMON_LZO" ]; then
+		/usr/bin/pluto-lzo d "$PROXY_DAEMON_LZO" /tmp/upnp-event-proxy.lua
+	fi
+	# Close file descriptors.
+	for fd in /proc/self/fd/*; do
+		fd=${fd##*/}
+		case $fd in
+			0|1|2) ;;
+			*) eval "exec $fd<&-"
+		esac
+	done
+	# Run daemon.
+	/usr/bin/lua "$PROXY_DAEMON" </dev/null >/dev/null 2>&1 &
+	echo "$!" > "$PID_FILE"
+}
+stop() {
+	if [ -f "$PID_FILE" ]; then
+		PID=$(cat "$PID_FILE")
+		if [ -d "/proc/$PID" ]; then
+			COMMAND=$(readlink "/proc/$PID/exe")
+			if [ "$COMMAND" = "/usr/bin/lua" ]; then
+				/bin/kill "$PID" && /bin/rm "$PID_FILE"
+				return 0
+			fi
+		fi
+	fi
+	echo "Daemon is not running"
+	return 1
+}
+]=]
+
+local initScriptPath = "/etc/init.d/upnp-proxy-daemon"
+
+local ProxyServiceId = "urn:futzle-com:serviceId:UPnPProxy1"
+local DeviceId
+local taskHandle = -1
+
+function task(message, mode)
+	taskHandle = luup.task(message, mode, string.format("%s[%d]", luup.devices[DeviceId].description, DeviceId), taskHandle)
+end
+
+function createInitScript()
+	task("Creating init script", 1)
+	local f = io.open(initScriptPath, "w")
+	f:write(initScript)
+	f:close()
+	task("Created init script", 4)
+	task("Making init script executable", 1)
+	os.execute("chmod +x " .. initScriptPath)
+	task("Made init script executable", 4)
+	task("Enabling init script", 1)
+	os.execute(initScriptPath .. " enable")
+	task("Enabled init script", 4)
+	task("Starting init script", 1)
+	os.execute(initScriptPath .. " start")
+	task("Started init script", 4)
+end
+
+function updateProxyVersion()
+	-- Get API version.
+	task("Checking that proxy is running", 1)
+	local ProxyApiVersion
+	local t = {}
+	local request, code = http.request({
+		url = "http://localhost:2529/version",
+		sink = ltn12.sink.table(t)
+	})
+
+	if (request == nil and code == "timeout") then
+		task("Checking that proxy is running (retrying)", 1)
+		luup.call_delay("updateProxyVersion", 5, "")
+		return
+	elseif (request == nil and code ~= "closed") then
+		-- Proxy not running.
+		task("Proxy is not running", 4)
+		luup.variable_set(ProxyServiceId, "Status", 0, DeviceId)
+		luup.variable_set(ProxyServiceId, "StatusText", "Not Running", DeviceId)
+	else
+		-- Proxy is running, note its version number.
+		task("Proxy is running", 4)
+		ProxyApiVersion = table.concat(t)
+		luup.variable_set(ProxyServiceId, "Status", 1, DeviceId)
+		luup.variable_set(ProxyServiceId, "StatusText", "Running", DeviceId)
+		luup.variable_set(ProxyServiceId, "API", ProxyApiVersion, DeviceId)
+	end
+
+	-- Check again in a while.
+	luup.call_delay("updateProxyVersion", 400, "")
+end
+
+function initialize(deviceId)
+	DeviceId = deviceId
+	-- Create the init script in /etc/init.d/upnp-proxy-daemon.
+	local f = io.open(initScriptPath, "r")
+	if (f) then
+		-- File already exists.
+		if (f:read("*a") == initScript) then
+			luup.log("Init script unchanged.")
+			f:close()
+		else
+			luup.log("Init script different; will be recreated.")
+			f:close()
+			task("Stopping init script", 1)
+			os.execute(initScriptPath .. " stop")
+			task("Stopped init script", 4)
+			createInitScript()
+			luup.call_delay("restartNeeded", 1, "Restart Luup engine to complete installation")
+			return true
+		end
+	else
+		luup.log("Init script absent; will be created.")
+		createInitScript()
+		luup.call_delay("restartNeeded", 1, "Restart Luup engine to complete installation")
+		return true
+	end
+
+	-- Proxy should be running now.
+
+	luup.call_delay("updateProxyVersion", 1, "")
+	return true
+end
+
+function uninstall(deviceId)
+	local f = io.open(initScriptPath, "r")
+	if (f) then
+		-- File exists.
+		f:close()
+		task("Stopping init script", 1)
+		os.execute(initScriptPath .. " stop")
+		task("Stopped init script", 4)
+		task("Disabling init script", 1)
+		os.execute(initScriptPath .. " disable")
+		task("Disabling init script", 4)
+		task("Removing init script", 1)
+		os.execute("rm " .. initScriptPath)
+		task("Removed init script", 4)
+		task("Delete the UPnP Event Proxy device, then reload Luup engine.", 1)
+	end
+end
+
+function restartNeeded(message)
+	task(message, 2)
+end
Index: np-event-proxy.lua
===================================================================
--- /upnp-event-proxy.lua	(revision 1)
+++ 	(revision )
@@ -1,495 +1,0 @@
-#!/usr/bin/lua
---[[
-
-	UPnP event proxy
-	
-	Copyright 2013 Deborah Pickett
-
-	Parts of this program have been modified from the Luasocket library
-	(http://w3.impa.br/~diego/software/luasocket/), released under the MIT licence.
-
-	Version 0.1 2013-03-17 API version 1
-	Version 0.1 2013-04-07 API version 2 (locking)
-	Version 0.1 2013-04-09 API version 3 (locking replaced with random retries)
-
-	This program listens on a TCP port (2529) for a subset of HTTP requests.
-	
-	GET /version HTTP/1.1
-	Returns 200 OK, and the current API version of this program.
-
-	PUT /upnp/event/[SID] HTTP/1.1
-	Request that the proxy send notifications for the subscription [SID]
-	to a nominated MiOS device.  Notifications are sent when a variable's
-	value is sent by the UPnP device.
-		<subscription expiry="timestamp">
-			<variable name="UPnPVariableName" host="localhost" deviceId="123"
-				serviceId="serviceWithAction" action="deviceActionName"
-				parameter="deviceActionParameter"
-				sidParameter="deviceActionParameter"
-			/>
-			<!-- more <variable> entries here. -->
-		</subscription>
-		Parameters:
-			expiry: os.time() before which renewal must happen.
-			name: the UPnP variable that the device is interested in.
-			host: the address to forward the event to.  Defaults to localhost.
-			deviceId: the device number that wants to be informed of the event.
-			serviceId: the service Id of the action to invoke.
-			action: the name of the action to invoke on the target device.
-			parameter: the name of the parameter that will contain the value of the variable
-			sidParameter: the name of the parameter that will contain the SID.
-
-	DELETE /upnp/event/[SID] HTTP/1.1
-	Request that this program no longer forwards events for subscription [SID].
-
-	NOTIFY /upnp/event HTTP/1.1
-	Sent by the UPnP device.  Format is as specified in the UPnP spec.
-	Events that are subscribed to will be forwarded to the previously registered
-	device with the registered action.
-]]
-
-local socket = require('socket')
-local url = require('socket.url')
-local http = require('socket.http')
-local ltn12 = require('ltn12')
-local lxp = require('lxp')
-
-local API_VERSION = "3"
-local LISTEN_PORT = 2529
-local LISTEN_BACKLOG = 5
-local LISTEN_TIMEOUT = 10
-
-local subscriptions = {}
-local notificationQueue = {}
-
-function log(s)
-	print(s)
-end
-
--- Parse the HTTP request line.
--- Returns the method and the path (split at / characters)
-function readStatusLine(c)
-	local statusLine = c:receive('*l')
-	local method, path = statusLine:match("^(%u+) ([^ ]+) HTTP/1.[01]")
-	if (statusLine:match("^GET ")) then
-		return "GET", url.parse_path(path)
-	elseif (statusLine:match("^PUT ")) then
-		return "PUT", url.parse_path(path)
-	elseif (statusLine:match("^NOTIFY ")) then
-		return "NOTIFY", url.parse_path(path)
-	elseif (statusLine:match("^DELETE ")) then
-		return "DELETE", url.parse_path(path)
-	else
-		return nil, "405 Method not allowed"
-	end
-end
-
--- Parse the HTTP headers.
--- Returns them as a table.
-function readHeaders(c)
-	local headers = {}
-	local headerLine = c:receive('*l')
-	while (headerLine) do
-		local nextLine = c:receive('*l')
-		if (not nextLine) then
-			-- Error, should always be a blank line at least.	
-			return nil
-		end
-		if (nextLine:match("^%s")) then
-			headerLine = headerLine .. nextLine:gsub("%s+", " ", 1)
-		else
-			-- Header line does not start with space; previous header is complete.
-			local name, value = headerLine:match("^([^:]+): ?(.*)$")
-			-- log("header: " .. name)
-			-- log("value: " .. value)
-			headers[name:lower()] = value
-			if (nextLine == "") then
-				return headers
-			end
-			headerLine = nextLine
-		end
-	end
-end
-
--- Fetch the HTTP body.
-function readBody(c, len)
-	local result = ""
-	if (len) then
-		-- log("Reading " .. len .. " bytes")
-		local bytesToRead = len
-		while (bytesToRead > 0) do
-			local data, reason = c:receive(len)
-			if (data) then
-				bytesToRead = bytesToRead - data:len()
-				result = result .. data
-			else
-				return nil, reason
-			end
-		end
-	else
-		-- Untested!
-		log("Reading till EOF")
-		while (true) do
-			local data, reason = c:receive("*a")
-			log(data)
-			log(reason)
-			if (data) then
-				result = result .. data
-			else
-				return nil, reason
-			end
-		end
-	end
-
-	return result
-end
-
--- Add a notification to the notification queue.
--- This queue will be sent when the proxy process
--- is back in its main loop.
-function queueVariableNotification(sid, variable, value)
-	log("Queueing notification for " .. sid .. "/" .. variable.name .. " = " .. value)
-	table.insert(notificationQueue, {
-		delayUntil = os.time(),
-		retries = 3,
-		url = "http://" .. variable.host .. ":3480/data_request?id=lu_action" ..
-		"&DeviceNum=" .. url.escape(variable.deviceId) ..
-		"&serviceId=" .. url.escape(variable.serviceId) ..
-		"&action=" .. url.escape(variable.action) ..
-		"&" .. url.escape(variable.parameter) .. "=" .. url.escape(value) ..
-		(variable.sidParameter and ("&" .. url.escape(variable.sidParameter) .. "=" .. url.escape(sid)) or "")
-	})
-end
-
--- Ensure that a subscription is known about.
-function createSubscription(sid, expiry, clearProxy)
-	if (not subscriptions[sid]) then
-		-- Unknown subscription.  Keep it around in case someone asks.
-		if (not expiry) then
-			-- 60 seconds ought to be enough.
-			expiry = os.time() + 60
-		end
-		subscriptions[sid] = {
-			variables = {},
-			proxy = {},
-			expiry = expiry,
-		}
-	end
-	if (clearProxy) then
-		subscriptions[sid].proxy = {}
-	end
-end
-
--- Called when a UPnP device sends an event.
--- Store the value, and decide if a MiOS
--- plugin is interested in knowing about it.
-function updateSubscribedVariable(sid, name, value)
-	createSubscription(sid)
-	subscriptions[sid].variables[name] = value
-	log("Updated variable in subscription " .. sid .. ": " .. name .. " = " .. value)
-	-- Proxy target may have already checked in.
-	for _, v in pairs(subscriptions[sid].proxy) do
-		if (name == v.name) then
-			queueVariableNotification(sid, v, value)
-		end
-	end
-end
-
--- Called when a MiOS plugin tells us that it
--- has requested a subscription.
--- Store the callback information, and decide if
--- the device has already reported a value for this
--- variable.
-function addProxy(sid, variable)
-	createSubscription(sid)
-	table.insert(subscriptions[sid].proxy, variable)
-	if (subscriptions[sid].variables[variable.name]) then
-		-- Variable value is already known.  Queue up a notification.
-		queueVariableNotification(sid, variable, subscriptions[sid].variables[variable.name])
-	end
-end
-
--- Returns a parser object that handles a UPnP NOTIFY body.
-function createUpnpNotificationParser()
-	local currentXpathTable = {}
-
-	local currentXpath = function()
-		return "/" .. table.concat(currentXpathTable, "/")
-	end
-
-	local result = {}
-	local variableName = nil
-
-	local xmlParser = lxp.new({
-		CharacterData = function(parser, string)
-			if (variableName) then
-				result[variableName] = result[variableName] .. string
-			end
-	 	end,
-		StartElement = function(parser, elementName, attributes)
-			-- log("Start element: " .. elementName)
-			table.insert(currentXpathTable, elementName)
-			if (#currentXpathTable == 3 and currentXpathTable[1] == "urn:schemas-upnp-org:event-1-0|propertyset" and currentXpathTable[2] == "urn:schemas-upnp-org:event-1-0|property") then
-				-- log("Variable: " .. elementName)
-				variableName = elementName
-				result[variableName] = ""
-			end
-		end,
-		EndElement = function(parser, elementName)
-			table.remove(currentXpathTable)
-			variableName = nil
-		end,
-	}, "|")
-
-	return {
-		parse = function(this, s) return xmlParser:parse(s) end,
-		close = function(this) xmlParser:close() end,
-		result = function(this) return result end,
-	}
-end
-
--- Returns a parser object that handles PUT requests
--- made by a MiOS plugin wanting to be informed of events.
-function createProxyRequestParser()
-	local currentXpathTable = {}
-
-	local currentXpath = function()
-		return "/" .. table.concat(currentXpathTable, "/")
-	end
-
-	local variables = {}
-	local expiry
-
-	local xmlParser = lxp.new({
-		StartElement = function(parser, elementName, attributes)
-			-- log("Start element: " .. elementName)
-			table.insert(currentXpathTable, elementName)
-			if (#currentXpathTable == 1 and currentXpathTable[1] == "subscription") then
-				expiry = attributes.expiry;
-			elseif (#currentXpathTable == 2 and currentXpathTable[1] == "subscription" and currentXpathTable[2] == "variable") then
-				local variable = {}
-				variable.name = attributes.name
-				variable.host = attributes.host
-				variable.deviceId = attributes.deviceId
-				variable.serviceId = attributes.serviceId
-				variable.action = attributes.action
-				variable.parameter = attributes.parameter
-				variable.sidParameter = attributes.sidParameter
-				table.insert(variables, variable)
-			end
-		end,
-		EndElement = function(parser, elementName)
-			table.remove(currentXpathTable)
-		end,
-	}, "|")
-
-	return {
-		parse = function(this, s) return xmlParser:parse(s) end,
-		close = function(this) xmlParser:close() end,
-		expiry = function(this) return expiry end,
-		variables = function(this) return variables end,
-	}
-end
-
--- Handle all PUT requests:
---   MiOS plugin is telling us of a subscription that it made.
-function handlePutRequest(c, path, headers, body)
-	if (#path == 3 and path[1] == "upnp" and path[2] == "event") then
-		-- Plugin is asking to be sent notifications for this subscription.
-		local sid = path[3]
-		local proxyRequestParser = createProxyRequestParser()
-		local result, reason = proxyRequestParser:parse(body)
-		proxyRequestParser:close()
-		if (result) then
-			local variables = proxyRequestParser:variables()
-			local expiry = proxyRequestParser:expiry()
-			createSubscription(sid, tonumber(expiry), true)
-			for i = 1, #variables do
-				log ("Will forward events for " .. variables[i].name)
-				addProxy(sid, variables[i])
-			end
-			c:send("HTTP/1.1 200 OK\r\n\r\n")
-		else
-			log("Parser failed: " .. reason)
-			c:send("HTTP/1.1 412 Precondition Failed\r\n\r\n")
-		end
-	else
-		c:send("HTTP/1.1 403 Forbidden\r\n\r\n")
-	end
-	return true
-end
-
--- Handle all GET requests:
---   API version of this web server.
-function handleGetRequest(c, path, headers)
-	if (#path == 1 and path[1] == "version") then
-		-- GET /version
-		-- Returns the API version.
-		c:send("HTTP/1.1 200 OK\r\n" .. "Content-Length: " .. API_VERSION:len() .. "\r\nContent-Type: text/plain\r\n\r\n" .. API_VERSION)
-	else
-	  c:send("HTTP/1.1 404 Not found\r\n\r\n")
-	end
-	return true
-end
-
--- Handle all DELETE requests:
---   MiOS plugin doesn't want to be informed about a subscription.
-function handleDeleteRequest(c, path, headers)
-	if (#path == 3 and path[1] == "upnp" and path[2] == "event") then
-		-- Plugin is asking to no longer be sent notifications for this subscription.
-		local sid = path[3]
-		subscriptions[sid] = nil
-		c:send("HTTP/1.1 200 OK\r\n\r\n")
-	else
-		c:send("HTTP/1.1 404 Not Found\r\n\r\n")
-	end
-	return true
-end
-
--- Handle all NOTIFY requests:
---   UPnP device is sending an event that a MiOS plugin subscribed to.
-function handleNotifyRequest(c, path, headers, body)
-	if (#path == 2 and path[1] == "upnp" and path[2] == "event") then
-		-- NOTIFY /upnp/event
-		-- UPnP device is informing of a state change.
-		-- UPnP spec deviation: WeMo devices insert NUL bytes in the XML.
-		body = body:gsub("%z", " ")
-		local upnpParser = createUpnpNotificationParser()
-		local result, reason = upnpParser:parse(body)
-		upnpParser:close()
-		if (result) then
-			-- log("Parser succeeded")
-			for k, v in pairs(upnpParser.result()) do
-				updateSubscribedVariable(headers["sid"], k, v)
-			end
-			c:send("HTTP/1.1 200 OK\r\n\r\n")
-		else
-			log("Parser failed: " .. reason)
-			c:send("HTTP/1.1 412 Precondition Failed\r\n\r\n")
-		end
-	else
-		c:send("HTTP/1.1 404 Not Found\r\n\r\n")
-	end
-	return true
-end
-
--- Handle an incoming HTTP request.
-function handleRequest(c, method, path, headers, body)
-	if (method == "PUT") then
-		return handlePutRequest(c, path, headers, body)
-	elseif (method == "GET") then
-		return handleGetRequest(c, path, headers)
-	elseif (method == "DELETE") then
-		return handleDeleteRequest(c, path, headers)
-	elseif (method == "NOTIFY") then
-		return handleNotifyRequest(c, path, headers, body)
-	else
-		c:send("405 Method Not Allowed\r\n\r\n")
-		return true
-	end
-end
-
--- Send outstanding notifications to MiOS plugins.
-function processNotificationQueue()
-	local saveForLater = {}
-	local nextTimeout = LISTEN_TIMEOUT
-	while (#notificationQueue > 0) do
-		local notification = table.remove(notificationQueue, 1)
-		if (notification.delayUntil > os.time()) then
-			-- Don't send yet, device may not be ready.
-			log("Event not ready, try again later")
-			table.insert(saveForLater, notification)
-		else
-			log("Sending notification: " .. notification.url)
-			local request, reason = http.request(notification.url)
-			-- Oh god, string matching.  "Device Not Ready" still comes with a 200 OK.
-			if (request and not(request:match("ERROR: Device not ready"))) then
-				-- Successful notification.
-				-- log("Response: " .. request)
-			else
-				log("Error: " .. reason)
-				if (notification.retries > 0) then
-					local randomDelay = math.random(1, 5)
-					notification.delayUntil = os.time() + randomDelay
-					notification.retries = notification.retries - 1
-					nextTimeout = math.min(nextTimeout, randomDelay)
-					table.insert(saveForLater, notification)
-				end
-			end
-			-- Wait at most ten seconds, at least 1.
-			nextTimeout = math.min(nextTimeout, LISTEN_TIMEOUT)
-			nextTimeout = math.max(nextTimeout, 1)
-		end
-	end
-	-- Unsuccessful notifications will be requeued for next time.
-	for _, notification in pairs(saveForLater) do
-		table.insert(notificationQueue, notification)
-	end
-	return nextTimeout
-end
-
--- Run until instructed to exit (not yet implemented)
-local runServer = true
-
-local s = socket.tcp()
-s:setoption("reuseaddr", true)
--- Every LISTEN_TIMEOUT seconds deal with retried notifications.
-s:settimeout(LISTEN_TIMEOUT)
-local result, reason = s:bind('*', LISTEN_PORT)
-if (not result) then
-	log("Cannot bind to port: " .. reason)
-	os.exit(1)
-end
-result, reason = s:listen(LISTEN_BACKLOG)
-if (not result) then
-	log("Cannot listen: " .. reason)
-	os.exit(1)
-end
-
-repeat
-	-- Wait (until timeout, perhaps) for a connection.
-	local c = s:accept()
-	if (c) then
-		local remoteHost = c:getpeername()
-		-- log("Connection established from " .. remoteHost)
-
-		local method, path = readStatusLine(c)
-		if (method) then
-			local logstring = os.date() .. " " .. remoteHost .. " > " .. method .. " "
-			for i = 1, #path do
-				logstring = logstring .. "/" .. path[i]
-			end
-			log(logstring)
-			-- So far so good.
-			local headers = readHeaders(c)
-			if (headers) then
-				-- Now read body.
-				local body
-				if (method == "GET" or method == "DELETE") then
-					body = nil
-				elseif (headers["content-length"]) then
-					body = readBody(c, tonumber(headers["content-length"]))
-				else
-					body = readBody(c, nil)
-				end
-				-- Dispatch the request.
-				runServer = handleRequest(c, method, path, headers, body)
-			else
-				log("Error while processing headers")
-				c:send("HTTP/1.1 400 Bad Request\r\n\r\n")
-			end
-		else
-			-- path contains the error code.
-			log("Error while processing request line: " .. path)
-			c:send("HTTP/1.1 500 " .. path .. "\r\n\r\n")
-		end
-		c:close()
-
-	end
-
-	-- Send outstanding notifications.
-	s:settimeout(processNotificationQueue())
-until not runServer
-
-s:close()
-
