Arduino Garage Controller
This is my first Instructable, so be easy on me! :-)
Although there are many garage door projects on Instructables using Arduinos, I needed/wanted something different. Last year, we had a warm summer and when I would come home after work, I would leave the garage door open about 1 foot so it could cool off. The trouble was that several nights I left the garage door open overnight :-( So I thought, I could use an Arduino with a real-time clock (RTC) to automatically close the garage door at 9 pm. So I built the first version of a garage controller. I used two sensors, one for "door is closed" and the other for "door is fully open" and a relay. The controller worked quite well for the rest of the summer.
When winter came, I decided to unplug the garage controller since I would leave the garage door partially open. This year when it started getting warm again, I plugged in the garage controller again. The trouble was that the RTC was not very accurate and the time was off. The only way to correct the time was to plug my laptop via USB to the garage controller, a pain because I had installed the garage controller on top of the garage door opener. So I had to climb a ladder with my laptop, connect the USB port to the Arduino, upload a "new" sketch that had to correct time and then upload the regular sketch (that didn't set the RTC).
In the meantime, I had bought a factory refurbished Vera2 "Smart Home Controller" from Mi Casa Verde on eBay. I had also found a Z-Wave home thermostat at Fry's for $14 so I could automatically set schedules for the heating and air conditioning. The Vera also allows me to remotely control the thermostat from my cell phone using one of the many apps that talk to the Vera.
Given that I had the Vera (that keeps accurate time) and the fact that you can write your own "plugins" for the Vera, I thought, I should connect my garage controller to the Vera. Once I connected my garage controller, I thought, Hey, I have an Arduino in the garage, what else can I control? So I decided to add more relays to control my irrigation system and replace the timer I have in the garage. Have you ever tried to manually turn on one zone with today's timers? Now, with my cell phone, I just tap a button!
The garage controller connects to the Vera2 via Ethernet. I'm using an Ethernet shield because they are less expensive than WiFi shields.
I could have used a Raspberry Pi but since its GPIO are 3.3V I decided to stick with the Arduino.
Although there are many garage door projects on Instructables using Arduinos, I needed/wanted something different. Last year, we had a warm summer and when I would come home after work, I would leave the garage door open about 1 foot so it could cool off. The trouble was that several nights I left the garage door open overnight :-( So I thought, I could use an Arduino with a real-time clock (RTC) to automatically close the garage door at 9 pm. So I built the first version of a garage controller. I used two sensors, one for "door is closed" and the other for "door is fully open" and a relay. The controller worked quite well for the rest of the summer.
When winter came, I decided to unplug the garage controller since I would leave the garage door partially open. This year when it started getting warm again, I plugged in the garage controller again. The trouble was that the RTC was not very accurate and the time was off. The only way to correct the time was to plug my laptop via USB to the garage controller, a pain because I had installed the garage controller on top of the garage door opener. So I had to climb a ladder with my laptop, connect the USB port to the Arduino, upload a "new" sketch that had to correct time and then upload the regular sketch (that didn't set the RTC).
In the meantime, I had bought a factory refurbished Vera2 "Smart Home Controller" from Mi Casa Verde on eBay. I had also found a Z-Wave home thermostat at Fry's for $14 so I could automatically set schedules for the heating and air conditioning. The Vera also allows me to remotely control the thermostat from my cell phone using one of the many apps that talk to the Vera.
Given that I had the Vera (that keeps accurate time) and the fact that you can write your own "plugins" for the Vera, I thought, I should connect my garage controller to the Vera. Once I connected my garage controller, I thought, Hey, I have an Arduino in the garage, what else can I control? So I decided to add more relays to control my irrigation system and replace the timer I have in the garage. Have you ever tried to manually turn on one zone with today's timers? Now, with my cell phone, I just tap a button!
The garage controller connects to the Vera2 via Ethernet. I'm using an Ethernet shield because they are less expensive than WiFi shields.
I could have used a Raspberry Pi but since its GPIO are 3.3V I decided to stick with the Arduino.
Parts & Tools
So here's are the parts I used:
- Arduino Uno
- An Ethernet Shield (any shield that works with your Arduino)
- A 4 Channel 5 volt relay module
- A PC board
- A fuse holder for the 24V used by the irrigation valves
- A polarized connector for the 24V
- A DB9 male connector with flat ribbon cable (I had laying around)
- Miscellaneous screws and bolts
- A plastic box to hold the controller.
- Various straight and right angle headers
- Wire-wrap wire (I had laying around)
- Magnet wire (for the 24V)
- 2 Switches with NO and NC connections
- Speaker or telephone wire to connect sensors
- 2 conductor connectors
- Soldering Iron
- Solder
- Drill and bits
- Files
- Labeler (optional)
- Arduino Development Software
- A text editor
Schematic/Block Diagram
Here's the schematic/block diagram.
Arduino Uno, Ethernet Shield & 4 Channel Relay Module
By trial & error, I first mounted the Arduino Uno to the bottom half of the plastic box by drilling on the bottom. I had to cut and file away the plastic to allow for the USB connector. I used spacers to hold the Arduino Uno to the bottom. In a similar fashion, I attached the 4 channel relay module to the bottom half of the plastic box.
I also drill/cut/filed the holes in the top half of the plastic box for the Ethernet connector, the 24V connector, the DB9 connector and for the sensors and switch headers.
In the first photo below, I've already attached the wires from the DB9 connector.
I also drill/cut/filed the holes in the top half of the plastic box for the Ethernet connector, the 24V connector, the DB9 connector and for the sensors and switch headers.
In the first photo below, I've already attached the wires from the DB9 connector.
Breadboard
The breadboard is what connects the Arduino/Ethernet Shield to the relay module and the "outside" world.
Door Sensors and Pushbutton
I mounted the 2 switches at each end of the garage door rail. Since I wanted to sense the "normal state" (i.e. the garage door is closed) as HIGH on the Arduino, the closed door sensor is connected to the NO pin and the fully open sensor is connected to the NC pin on the switches.
To open and close the garage door, I then spliced a wire from a relay in the garage controller to the wire coming from the pushbutton on the wall.
To open and close the garage door, I then spliced a wire from a relay in the garage controller to the wire coming from the pushbutton on the wall.
The Arduino Code
Here's the code running on the Arduino:
/*
Garage Controller
Written by Aram Perez
Licensed under GPLv2, available at http://www.gnu.org/licenses/gpl-2.0.txt
*/
//#define LOG_SERIAL
#include <SPI.h>
#include <Ethernet.h>
#include <Wire.h>
#define NO_PORTA_PINCHANGES
#define NO_PORTC_PINCHANGES
#include <PinChangeInt.h>
#define IOPORT 23 //Normal telnet port
#define NBR_OF_RELAYS 4
// Garage door sensors & pushbutton
#define GARAGE_CLOSED_SENSOR 2 //Connect to NC terminal, active high
#define GARAGE_PARTIALLY_OPEN_SENSOR 3 //Connect to NO terminal, active high
#define RELAY0 4
#define GARAGE_RELAY RELAY0 //Relay for garage door button
#define RELAY1 5
#define RELAY2 6
#define RELAY3 7
#define CR ((char)13)
#define LF ((char)10)
// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network.
// gateway and subnet are optional:
static byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
static IPAddress ip(192, 168, 1, 170);
static IPAddress gateway(192, 168, 1, 1);
static IPAddress subnet(255, 255, 255, 0);
static EthernetServer server(IOPORT);
static EthernetClient client;
static char relayState[NBR_OF_RELAYS];
class GarageDoor
{
bool closedState, partiallyOpenState;
public:
GarageDoor();
void Init();
void SetClosedState(bool st){
closedState = st;
}
void SetPartiallyOpenState(bool st){
partiallyOpenState = st;
}
char State() const;
void PushButton();
};
static GarageDoor garageDoor;
//This should be a private function in the GarageDoor class
//i.e. GarageDoor::StateChangedISR(void),
//but the compiler gives an error if it is :-(
static void StateChangedISR(void)
{
if( PCintPort::arduinoPin == GARAGE_CLOSED_SENSOR ){
garageDoor.SetClosedState(PCintPort::pinState);
}
else{
//Must have been the GARAGE_PARTIALLY_OPEN_SENSOR:
garageDoor.SetPartiallyOpenState(PCintPort::pinState);
}
}
GarageDoor::GarageDoor()
{
}
void GarageDoor::Init()
{
pinMode(GARAGE_CLOSED_SENSOR, INPUT_PULLUP);
PCintPort::attachInterrupt(GARAGE_CLOSED_SENSOR, &StateChangedISR, CHANGE);
pinMode(GARAGE_PARTIALLY_OPEN_SENSOR, INPUT_PULLUP);
PCintPort::attachInterrupt(GARAGE_PARTIALLY_OPEN_SENSOR, &StateChangedISR, CHANGE);
closedState = digitalRead(GARAGE_CLOSED_SENSOR);
partiallyOpenState = digitalRead(GARAGE_PARTIALLY_OPEN_SENSOR);
}
void GarageDoor::PushButton()
{
digitalWrite(GARAGE_RELAY, LOW);
delay(400); //Delay .4 secs
digitalWrite(GARAGE_RELAY, HIGH);
}
char GarageDoor::State() const
{
if( closedState ) return 'c';
return partiallyOpenState ? 'p' : 'o';
}
void setup() {
#ifdef LOG_SERIAL
Serial.begin(56700);
#endif
// initialize the ethernet device
Ethernet.begin(mac, ip, gateway, subnet);
// start listening for clients
server.begin();
garageDoor.Init();
for( int i = 0; i < NBR_OF_RELAYS; i++ ){
pinMode(RELAY0+i, OUTPUT); //Zone 1
digitalWrite(RELAY0+i, HIGH); //Relays use inverted logic, HIGH = Off
relayState[i] = '0'; //Use normal logic
}
if( client.connected() ){
client.flush();
}
#ifdef LOG_SERIAL
Serial.println("\r\nOK");
#endif
}
char ReadNext()
{
char ch = client.read();
#ifdef LOG_SERIAL
Serial.print(ch);
#endif
return ch;
}
//
//Commands:
// g? - return current garage door state
// c - door is closed
// o - door is fully open
// p - door is partially open
// gb - "push" garage door button
// rx? - return relay x state
// rxy - set relay x to y (0 or 1)
//
void loop() {
static char lastGarageDoorState = 'c';
char ch, rAsc;
if( !client.connected() ){
// If client is not connected, wait for a new client:
client = server.available();
}
if( client.available() > 0 ){
int rNdx;
bool err = false;
while( client.available() > 0 ){
switch ( ReadNext() ) {
case 'g':
switch ( ReadNext() ) {
case '?':
ch = garageDoor.State();
client.print('g');
client.println(ch);
#ifdef LOG_SERIAL
Serial.print(">g");
Serial.println(ch);
#endif
break;
case 'b':
garageDoor.PushButton();
break;
default:
err = true;
}
break;
case 'r':
ch = ReadNext();
switch( ch ){
case '1':
case '2':
case '3':
rAsc = ch;
rNdx = ch - '1';
ch = ReadNext();
switch( ch ){
case '?':
ch = relayState[rNdx];
break;
case '0':
digitalWrite(RELAY1 + rNdx, HIGH); //Inverted logic
relayState[rNdx] = ch;
break;
case '1':
digitalWrite(RELAY1 + rNdx, LOW); //Inverted logic
relayState[rNdx] = ch;
break;
default:
err = true;
}
if( !err ){
client.print('r');
client.print(rAsc);
client.println(ch);
#ifdef LOG_SERIAL
Serial.print('>');
Serial.println(ch);
#endif
}
break;
default:
err = true;
}
break;
case CR:
case LF:
break; //Ignore CR & LF
default:
err = true;
}
}
if( err ){
client.println('?');
#ifdef LOG_SERIAL
Serial.println(">Say what?");
#endif
}
}
ch = garageDoor.State();
if( ch != lastGarageDoorState ){
lastGarageDoorState = ch;
client.print('g');
client.println(ch);
#ifdef LOG_SERIAL
Serial.print(">g");
Serial.println(ch);
#endif
}
}
/*
Garage Controller
Written by Aram Perez
Licensed under GPLv2, available at http://www.gnu.org/licenses/gpl-2.0.txt
*/
//#define LOG_SERIAL
#include <SPI.h>
#include <Ethernet.h>
#include <Wire.h>
#define NO_PORTA_PINCHANGES
#define NO_PORTC_PINCHANGES
#include <PinChangeInt.h>
#define IOPORT 23 //Normal telnet port
#define NBR_OF_RELAYS 4
// Garage door sensors & pushbutton
#define GARAGE_CLOSED_SENSOR 2 //Connect to NC terminal, active high
#define GARAGE_PARTIALLY_OPEN_SENSOR 3 //Connect to NO terminal, active high
#define RELAY0 4
#define GARAGE_RELAY RELAY0 //Relay for garage door button
#define RELAY1 5
#define RELAY2 6
#define RELAY3 7
#define CR ((char)13)
#define LF ((char)10)
// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network.
// gateway and subnet are optional:
static byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
static IPAddress ip(192, 168, 1, 170);
static IPAddress gateway(192, 168, 1, 1);
static IPAddress subnet(255, 255, 255, 0);
static EthernetServer server(IOPORT);
static EthernetClient client;
static char relayState[NBR_OF_RELAYS];
class GarageDoor
{
bool closedState, partiallyOpenState;
public:
GarageDoor();
void Init();
void SetClosedState(bool st){
closedState = st;
}
void SetPartiallyOpenState(bool st){
partiallyOpenState = st;
}
char State() const;
void PushButton();
};
static GarageDoor garageDoor;
//This should be a private function in the GarageDoor class
//i.e. GarageDoor::StateChangedISR(void),
//but the compiler gives an error if it is :-(
static void StateChangedISR(void)
{
if( PCintPort::arduinoPin == GARAGE_CLOSED_SENSOR ){
garageDoor.SetClosedState(PCintPort::pinState);
}
else{
//Must have been the GARAGE_PARTIALLY_OPEN_SENSOR:
garageDoor.SetPartiallyOpenState(PCintPort::pinState);
}
}
GarageDoor::GarageDoor()
{
}
void GarageDoor::Init()
{
pinMode(GARAGE_CLOSED_SENSOR, INPUT_PULLUP);
PCintPort::attachInterrupt(GARAGE_CLOSED_SENSOR, &StateChangedISR, CHANGE);
pinMode(GARAGE_PARTIALLY_OPEN_SENSOR, INPUT_PULLUP);
PCintPort::attachInterrupt(GARAGE_PARTIALLY_OPEN_SENSOR, &StateChangedISR, CHANGE);
closedState = digitalRead(GARAGE_CLOSED_SENSOR);
partiallyOpenState = digitalRead(GARAGE_PARTIALLY_OPEN_SENSOR);
}
void GarageDoor::PushButton()
{
digitalWrite(GARAGE_RELAY, LOW);
delay(400); //Delay .4 secs
digitalWrite(GARAGE_RELAY, HIGH);
}
char GarageDoor::State() const
{
if( closedState ) return 'c';
return partiallyOpenState ? 'p' : 'o';
}
void setup() {
#ifdef LOG_SERIAL
Serial.begin(56700);
#endif
// initialize the ethernet device
Ethernet.begin(mac, ip, gateway, subnet);
// start listening for clients
server.begin();
garageDoor.Init();
for( int i = 0; i < NBR_OF_RELAYS; i++ ){
pinMode(RELAY0+i, OUTPUT); //Zone 1
digitalWrite(RELAY0+i, HIGH); //Relays use inverted logic, HIGH = Off
relayState[i] = '0'; //Use normal logic
}
if( client.connected() ){
client.flush();
}
#ifdef LOG_SERIAL
Serial.println("\r\nOK");
#endif
}
char ReadNext()
{
char ch = client.read();
#ifdef LOG_SERIAL
Serial.print(ch);
#endif
return ch;
}
//
//Commands:
// g? - return current garage door state
// c - door is closed
// o - door is fully open
// p - door is partially open
// gb - "push" garage door button
// rx? - return relay x state
// rxy - set relay x to y (0 or 1)
//
void loop() {
static char lastGarageDoorState = 'c';
char ch, rAsc;
if( !client.connected() ){
// If client is not connected, wait for a new client:
client = server.available();
}
if( client.available() > 0 ){
int rNdx;
bool err = false;
while( client.available() > 0 ){
switch ( ReadNext() ) {
case 'g':
switch ( ReadNext() ) {
case '?':
ch = garageDoor.State();
client.print('g');
client.println(ch);
#ifdef LOG_SERIAL
Serial.print(">g");
Serial.println(ch);
#endif
break;
case 'b':
garageDoor.PushButton();
break;
default:
err = true;
}
break;
case 'r':
ch = ReadNext();
switch( ch ){
case '1':
case '2':
case '3':
rAsc = ch;
rNdx = ch - '1';
ch = ReadNext();
switch( ch ){
case '?':
ch = relayState[rNdx];
break;
case '0':
digitalWrite(RELAY1 + rNdx, HIGH); //Inverted logic
relayState[rNdx] = ch;
break;
case '1':
digitalWrite(RELAY1 + rNdx, LOW); //Inverted logic
relayState[rNdx] = ch;
break;
default:
err = true;
}
if( !err ){
client.print('r');
client.print(rAsc);
client.println(ch);
#ifdef LOG_SERIAL
Serial.print('>');
Serial.println(ch);
#endif
}
break;
default:
err = true;
}
break;
case CR:
case LF:
break; //Ignore CR & LF
default:
err = true;
}
}
if( err ){
client.println('?');
#ifdef LOG_SERIAL
Serial.println(">Say what?");
#endif
}
}
ch = garageDoor.State();
if( ch != lastGarageDoorState ){
lastGarageDoorState = ch;
client.print('g');
client.println(ch);
#ifdef LOG_SERIAL
Serial.print(">g");
Serial.println(ch);
#endif
}
}
The Vera Code
To use my Garage Controller with my Vera2, I had to write a "plugin". But adding your own plugin for the Vera is not easy. First, the little documentation that exists on their Wiki is either out of date or incomplete. There is also a forum where you can see what other people have done and ask questions.
Vera uses a combination of UPnP and LUA called Luup. You need at least two files, a "definition" file and an "implementation" file. The trouble is that the implementation file is a combination of XML and LUA. The only way to test your LUA code (at least that I'm aware of for the Mac), is to load the implementation file and hope it runs. Loading your files and restarting the Luup engine takes a minute or more, so the process is slow. There is no debugger and your only debugging tool is the logging facility. You view the log, you can either SSH into the Vera or use the following URL: <yourVeraIp>/cgi-bin/cmh/log.sh?Device=LuaUPnP". If there are easier ways, I have not found them yet.
Unless your device is a "well known" UPnP type, all the cell phone remote control apps will not be able to control your device. Since I want to do remote control, my Garage Controller appears as a "Garage Controller" that has the following child devices:
<?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-aram-perez-com:device:GarageController:1</deviceType>
<friendlyName>Garage Controller</friendlyName>
<modelNumber>1.0</modelNumber>
<protocol>crlf</protocol>
<handleChildren>1</handleChildren>
<implementationList>
<implementationFile>I_GarageController1.xml</implementationFile>
</implementationList>
</device>
</root>
And here is the Implementation File (save as "I_GarageController1.xml"):
<?xml version="1.0"?>
<implementation>
<functions>
local GC = "Garage Controller, device: "
local GC_SID = "urn:schemas-aram-perez-com:device:GarageController:1"
local SP_SID = "urn:upnp-org:serviceId:SwitchPower1"
local DIM_SID = "urn:upnp-org:serviceId:Dimming1"
local CR = string.char(13)
local FIXED_LEVEL = "30"
local CSI = string.char(27, 91) --ESC+[
local parentDevice
local garageDoorStatus
-- -------------------------------------------------------------------------
-- Log with color
function Log(device, msg)
luup.log(CSI .. "35m" .. GC .. tostring(device) .. ", " .. msg .. CSI .. "0m")
end
function LogL1(device, msg)
luup.log(CSI .. "35m" .. GC .. tostring(device) .. ", " .. msg .. CSI .. "0m", 1)
end
function LogL2(device, msg)
luup.log(CSI .. "35m" .. GC .. tostring(device) .. ", " .. msg .. CSI .. "0m", 2)
end
function startup(lul_device)
local device = luup.devices[lul_device]
local ipAddress, ignore, ipPort = string.match(device.ip, "^([%w%.%-]+)(:?(%d-))$")
if (ipAddress ~= "") then
parentDevice = lul_device
if (ipPort == nil) or (ipPort == "") then
if (device.port == nil) or (device.port == "") then
ipPort = 23;
end
end
Log(lul_device, ("starting up, connecting to " .. ipAddress .. ", port " .. ipPort))
luup.io.open(lul_device, ipAddress, ipPort)
child_devices = luup.chdev.start(lul_device);
luup.chdev.append(lul_device,child_devices,"GD", "Garage Door", "urn:schemas-upnp-org:device:DimmableLight:1", "D_DimmableLight1.xml", "", "", true)
luup.chdev.append(lul_device,child_devices,"Z1", "Irrigation Zone 1", "urn:schemas-upnp-org:device:BinaryLight:1", "D_BinaryLight1.xml", "", "", true)
luup.chdev.append(lul_device,child_devices,"Z2", "Irrigation Zone 2", "urn:schemas-upnp-org:device:BinaryLight:1", "D_BinaryLight1.xml", "", "", true)
luup.chdev.append(lul_device,child_devices,"Z3", "Irrigation Zone 3", "urn:schemas-upnp-org:device:BinaryLight:1", "D_BinaryLight1.xml", "", "", true)
luup.chdev.sync(lul_device,child_devices)
local value = luup.variable_get(GC_SID, "DelayPartialOpen", lul_device + 1)
if (value == nil) or (value == "") then
luup.variable_set(GC_SID, "DelayPartial Open", "3", lul_device + 1)
end
-- Assume all irrigation relays are off
luup.variable_set(GC_SID, "Status", "0", lul_device + 2)
luup.variable_set(GC_SID, "Status", "0", lul_device + 3)
luup.variable_set(GC_SID, "Status", "0", lul_device + 4)
else
local err = "ERROR: No IP address found"
LogL2(lul_device, err)
return false, err, "Garage Controller"
end
luup.io.write("g?")
return true, "Ok", "Garage Controller"
end
function partialOpen(data)
luup.io.write("gb")
end
</functions>
<startup>startup</startup>
<incoming>
<lua>
Log(lul_device, ("received data: " .. tostring(lul_data)))
local ch = lul_data:sub(1,1)
if ch == 'g' then
local status
ch = lul_data:sub(2,2)
if ch == 'o' then
garageDoorStatus = ch
status = "100"
luup.variable_set(SP_SID, "Status", "1", lul_device + 1)
elseif ch == 'c' then
garageDoorStatus = ch
status = "0"
luup.variable_set(SP_SID, "Status", "0", lul_device + 1)
elseif ch == 'p' then
garageDoorStatus = ch
status = FIXED_LEVEL
luup.variable_set(SP_SID, "Status", "1", lul_device + 1)
else
Log(lul_device, "unknown received data")
do return end
end
luup.variable_set(DIM_SID, "LoadLevelStatus", status, lul_device + 1)
elseif ch == 'r' then
ch = lul_data:sub(2,2)
if (ch > '0') and (ch < '4') then
luup.variable_set(SP_SID, "Status",lul_data:sub(3,3),lul_device + ch + 1)
else
LogL1(lul_device, ("invalid zone number: " .. tostring(device)))
end
else
LogL2(lul_device, "unknown data")
end
</lua>
</incoming>
<actionList>
<action>
<serviceId>urn:upnp-org:serviceId:Dimming1</serviceId>
<name>SetLoadLevelTarget</name>
<run>
local garageLevel = lul_settings.newLoadlevelTarget
luup.variable_set(DIM_SID, "LoadLevelTarget", garageLevel, lul_device)
Log(lul_device, ("setting door level to " .. garageLevel))
local status = luup.variable_get(SP_SID, "Status", lul_device)
if garageLevel == status then
return true
end
if luup.io.write("gb") == false then
LogL1(lul_device, ("error sending command"))
luup.set_failure(true)
return false
end
if (garageLevel ~= "0") and (garageLevel ~= "100") then
local delay = luup.variable_get(GC_SID, "DelayPartialOpen", lul_device)
luup.call_delay("partialOpen", delay, garageLevel)
end
return true
</run>
</action>
<action>
<serviceId>urn:upnp-org:serviceId:SwitchPower1</serviceId>
<name>SetTarget</name>
<run>
local relay = lul_device - parentDevice
if (relay < 1) or (relay > 4) then
LogL1(lul_device, ("not a valid relay number: " .. relay))
return false
end
relay = relay - 1
local newTarget = tostring(lul_settings.newTargetValue)
local command = ""
local status = luup.variable_get(SP_SID, "Status", lul_device)
if status == nil then
status = "?"
end
if relay == 0 then
if status ~= newTarget then
command = "gb"
end
else
command = "r" .. relay .. newTarget
end
Log(lul_device, ("sending: " .. command))
luup.variable_set(SP_SID, "Target", newTarget, lul_device)
if command == "" then
return true
end
if luup.io.write(command) == false then
LogL1(lul_device, "error sending command")
luup.set_failure(true)
return false
end
return true
</run>
</action>
</actionList>
</implementation>
On the Vera UI5 (I have tested this with earlier UIs), click the APPS tab, then click the "Develop Apps" sub-tab and then on "Luup files" on the left. You will see a list of current and a place to select files to upload. Once you upload the two files, you click on "Create device" on the left and fill in the information. Under "Description" I enter "zGarage Controller" so that it appears as the last device on the UI5 interface. Once the device is created, I recommend that you "Reload" so that all the child devices correctly display.
You can add schedules to open/close your garage door and your irrigation zones. You can download Vera mobile apps to your cell phone and control the garage door and irrigation zones from anywhere in the world!
Vera uses a combination of UPnP and LUA called Luup. You need at least two files, a "definition" file and an "implementation" file. The trouble is that the implementation file is a combination of XML and LUA. The only way to test your LUA code (at least that I'm aware of for the Mac), is to load the implementation file and hope it runs. Loading your files and restarting the Luup engine takes a minute or more, so the process is slow. There is no debugger and your only debugging tool is the logging facility. You view the log, you can either SSH into the Vera or use the following URL: <yourVeraIp>/cgi-bin/cmh/log.sh?Device=LuaUPnP". If there are easier ways, I have not found them yet.
Unless your device is a "well known" UPnP type, all the cell phone remote control apps will not be able to control your device. Since I want to do remote control, my Garage Controller appears as a "Garage Controller" that has the following child devices:
- A Dimmable Light for controlling the garage door (remember, I want to partially open the door)
- Three Light Switches for each of the relays that control my irrigation zones.
<?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-aram-perez-com:device:GarageController:1</deviceType>
<friendlyName>Garage Controller</friendlyName>
<modelNumber>1.0</modelNumber>
<protocol>crlf</protocol>
<handleChildren>1</handleChildren>
<implementationList>
<implementationFile>I_GarageController1.xml</implementationFile>
</implementationList>
</device>
</root>
And here is the Implementation File (save as "I_GarageController1.xml"):
<?xml version="1.0"?>
<implementation>
<functions>
local GC = "Garage Controller, device: "
local GC_SID = "urn:schemas-aram-perez-com:device:GarageController:1"
local SP_SID = "urn:upnp-org:serviceId:SwitchPower1"
local DIM_SID = "urn:upnp-org:serviceId:Dimming1"
local CR = string.char(13)
local FIXED_LEVEL = "30"
local CSI = string.char(27, 91) --ESC+[
local parentDevice
local garageDoorStatus
-- -------------------------------------------------------------------------
-- Log with color
function Log(device, msg)
luup.log(CSI .. "35m" .. GC .. tostring(device) .. ", " .. msg .. CSI .. "0m")
end
function LogL1(device, msg)
luup.log(CSI .. "35m" .. GC .. tostring(device) .. ", " .. msg .. CSI .. "0m", 1)
end
function LogL2(device, msg)
luup.log(CSI .. "35m" .. GC .. tostring(device) .. ", " .. msg .. CSI .. "0m", 2)
end
function startup(lul_device)
local device = luup.devices[lul_device]
local ipAddress, ignore, ipPort = string.match(device.ip, "^([%w%.%-]+)(:?(%d-))$")
if (ipAddress ~= "") then
parentDevice = lul_device
if (ipPort == nil) or (ipPort == "") then
if (device.port == nil) or (device.port == "") then
ipPort = 23;
end
end
Log(lul_device, ("starting up, connecting to " .. ipAddress .. ", port " .. ipPort))
luup.io.open(lul_device, ipAddress, ipPort)
child_devices = luup.chdev.start(lul_device);
luup.chdev.append(lul_device,child_devices,"GD", "Garage Door", "urn:schemas-upnp-org:device:DimmableLight:1", "D_DimmableLight1.xml", "", "", true)
luup.chdev.append(lul_device,child_devices,"Z1", "Irrigation Zone 1", "urn:schemas-upnp-org:device:BinaryLight:1", "D_BinaryLight1.xml", "", "", true)
luup.chdev.append(lul_device,child_devices,"Z2", "Irrigation Zone 2", "urn:schemas-upnp-org:device:BinaryLight:1", "D_BinaryLight1.xml", "", "", true)
luup.chdev.append(lul_device,child_devices,"Z3", "Irrigation Zone 3", "urn:schemas-upnp-org:device:BinaryLight:1", "D_BinaryLight1.xml", "", "", true)
luup.chdev.sync(lul_device,child_devices)
local value = luup.variable_get(GC_SID, "DelayPartialOpen", lul_device + 1)
if (value == nil) or (value == "") then
luup.variable_set(GC_SID, "DelayPartial Open", "3", lul_device + 1)
end
-- Assume all irrigation relays are off
luup.variable_set(GC_SID, "Status", "0", lul_device + 2)
luup.variable_set(GC_SID, "Status", "0", lul_device + 3)
luup.variable_set(GC_SID, "Status", "0", lul_device + 4)
else
local err = "ERROR: No IP address found"
LogL2(lul_device, err)
return false, err, "Garage Controller"
end
luup.io.write("g?")
return true, "Ok", "Garage Controller"
end
function partialOpen(data)
luup.io.write("gb")
end
</functions>
<startup>startup</startup>
<incoming>
<lua>
Log(lul_device, ("received data: " .. tostring(lul_data)))
local ch = lul_data:sub(1,1)
if ch == 'g' then
local status
ch = lul_data:sub(2,2)
if ch == 'o' then
garageDoorStatus = ch
status = "100"
luup.variable_set(SP_SID, "Status", "1", lul_device + 1)
elseif ch == 'c' then
garageDoorStatus = ch
status = "0"
luup.variable_set(SP_SID, "Status", "0", lul_device + 1)
elseif ch == 'p' then
garageDoorStatus = ch
status = FIXED_LEVEL
luup.variable_set(SP_SID, "Status", "1", lul_device + 1)
else
Log(lul_device, "unknown received data")
do return end
end
luup.variable_set(DIM_SID, "LoadLevelStatus", status, lul_device + 1)
elseif ch == 'r' then
ch = lul_data:sub(2,2)
if (ch > '0') and (ch < '4') then
luup.variable_set(SP_SID, "Status",lul_data:sub(3,3),lul_device + ch + 1)
else
LogL1(lul_device, ("invalid zone number: " .. tostring(device)))
end
else
LogL2(lul_device, "unknown data")
end
</lua>
</incoming>
<actionList>
<action>
<serviceId>urn:upnp-org:serviceId:Dimming1</serviceId>
<name>SetLoadLevelTarget</name>
<run>
local garageLevel = lul_settings.newLoadlevelTarget
luup.variable_set(DIM_SID, "LoadLevelTarget", garageLevel, lul_device)
Log(lul_device, ("setting door level to " .. garageLevel))
local status = luup.variable_get(SP_SID, "Status", lul_device)
if garageLevel == status then
return true
end
if luup.io.write("gb") == false then
LogL1(lul_device, ("error sending command"))
luup.set_failure(true)
return false
end
if (garageLevel ~= "0") and (garageLevel ~= "100") then
local delay = luup.variable_get(GC_SID, "DelayPartialOpen", lul_device)
luup.call_delay("partialOpen", delay, garageLevel)
end
return true
</run>
</action>
<action>
<serviceId>urn:upnp-org:serviceId:SwitchPower1</serviceId>
<name>SetTarget</name>
<run>
local relay = lul_device - parentDevice
if (relay < 1) or (relay > 4) then
LogL1(lul_device, ("not a valid relay number: " .. relay))
return false
end
relay = relay - 1
local newTarget = tostring(lul_settings.newTargetValue)
local command = ""
local status = luup.variable_get(SP_SID, "Status", lul_device)
if status == nil then
status = "?"
end
if relay == 0 then
if status ~= newTarget then
command = "gb"
end
else
command = "r" .. relay .. newTarget
end
Log(lul_device, ("sending: " .. command))
luup.variable_set(SP_SID, "Target", newTarget, lul_device)
if command == "" then
return true
end
if luup.io.write(command) == false then
LogL1(lul_device, "error sending command")
luup.set_failure(true)
return false
end
return true
</run>
</action>
</actionList>
</implementation>
On the Vera UI5 (I have tested this with earlier UIs), click the APPS tab, then click the "Develop Apps" sub-tab and then on "Luup files" on the left. You will see a list of current and a place to select files to upload. Once you upload the two files, you click on "Create device" on the left and fill in the information. Under "Description" I enter "zGarage Controller" so that it appears as the last device on the UI5 interface. Once the device is created, I recommend that you "Reload" so that all the child devices correctly display.
You can add schedules to open/close your garage door and your irrigation zones. You can download Vera mobile apps to your cell phone and control the garage door and irrigation zones from anywhere in the world!
Future Enhancements and Conclusion
I have some future enhancement that I'll start working on soon:
Regards,
Aram Perez
- Add an ultrasonic sensor and LED so that when I drive into the garage, the LED turns on when I've reached the correct spot in garage.
- Actually correlate the "dim level" with how open the garage door is (right now it's hard coded to 20%).
- Maybe I'll print a better enclosure with a 3D printer.
Regards,
Aram Perez