I actually built something similar a couple of years ago. But it had some flaws, and I didn't put it into production. My approach was different as it used an Arduino. I thought you shouldn't need to run a whole Linux stack to just send some HTTP requests. Well, that is right, but it was more difficult to get working. My device for some reason would fail 1 out of 3 times. I never investigated why. After making it, I realized I should've just used an SBC and had it run some curl commands. What you did looks much better overall. I'll post the code here in case anyone is interested in seeing a fail.
The other differences are: I used a 3 or 4 gang light switch size, so it would fit into the wall. I had some toggle switches that could be pulled, no authentication, a speaker to give an audible beep/tune when the request is sent correctly, and the LCD is just a 16 character type. Unfortunately, I don't have any pictures or diagrams of how it was setup at the moment
Actually, it should be easy for someone to use a 16 character LCD with your code to save costs. I know the higher res LCDs are at least $40+ while the character displays are much less. Though the hi res LCDs may be able to display monitors or do some other interesting things.
Code: Select all
/*
*
* ZM Switch
*
* A device for the layman to change
* the runstate of Zoneminder with
* a light switch.
*
*
*/
/*
* What it does:
*
* Listens for a switch to be pressed.
* If switch is pressed:
* Packet One:
* Authenticates to Zoneminder over ethernet.
* Note: if you don't have authentication enabled, you should
* comment this out and edit packet two.
* Packet Two:
* Sends API request with authenticated
* cookie. API request will differ depending
* upon which switch you press.
*
*
* Has an LCD Screen, and a speaker to indicate progress.
* Setup for a 3 gang Light switch wall plate. Four switches plus power switch.
* See repo for details.
*
*/
/*
* todo:
*
* test in production
*
* add easter egg
*
* layout pcb that connects to mega as a shield, and has
* pcb mount switches, lcd, speaker incorporated.
*
*
*
*
*
* directions:
*
* Use Arduino Mega.
*
* Use an ENC28J60 for ethernet. Use light switches for the
* actual switches. Or any switch. A speaker and LED is optional but
* recommended. LCD is also optional, but included by default.
*
* The switches to change state should be a momentary type. (on)-off.
* The power switch is a standard on-off switch.
*
* Use a bivar SW-1002, or another similar inline power switch for
* 2.1mm coaxial power jacks. Then you can quickly switch on and off.
* Add a fuse. eBay has cheap inline power fuses. You can also use a fuse wire.
* Requires soldering.
*
*
* Don't leave this on.
* to use, power it on, wait for ready chime, flip the switch you want,
* wait for the LED and tune
* to signify it sent req successfully. Then power it off.
*
*
*
* PIN CONNECTIONS (adjustable of course):
*
* LCD: standard Arduino connections. See online.
* Note: I used an 8x2 LCD for my display. If you use 16x2, change
* lcd.begin
*
* Speaker Pin: Digital 8
*
* Switches: Digital 22-25, then connected to gnd
* When switch is activated, it goes LOW. Otherwise it sits HIGH due to input_pullups
* See: https://www.arduino.cc/en/Tutorial/DigitalPins
*
* Ethernet: I used Mega, which has ethernet on pins 50-53.
* The cheapo enc28j60 module i used required 5v. I think it has a vdivider.
* no vreg is visible on the board. 3.3 did not work, led didn't light.
*
*
*
*
*
*
*/
#include <UIPEthernet.h>
#include <EEPROM.h>
#include <LiquidCrystal.h>
#include <LcdBarGraph.h>
// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
byte lcdNumCols = 8; // -- number of columns in the LCD for bar graph
LcdBarGraph lbg(&lcd, lcdNumCols, 0, 1);
//Edit these values
//mac must be unique
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
//IP of zoneavr switch
byte ip[] = { 192, 168, 1, 177 };
//IP of zm server
byte server[] = { 192, 168, 1, 178 };
//ZM server ip to put in requests.
//maybe you can use hostname, not sure. TODO: test hostnames
String host="192.168.1.178";
//username and password to login to Zoneminder Server.
//If you don't have authentication, you will need to edit the
//script.
String username="username";
String password="password";
//Specify API req here.
//Switch1 == APIREQUEST1, Switch2 == APIREQUEST2, etc...
String APIREQUEST1="/zm/api/states.json";
String APIREQUEST2="/zm/api/host/getVersion.json";
String APIREQUEST3="/zm/api/monitors/10.json";
String APIREQUEST4="/zm/api/monitors/12.json";
//Pins
#define SWITCHONE 22
#define SWITCHTWO 23
#define SWITCHTHREE 24
#define SWITCHFOUR 25
#define SPEAKER_PIN 8
#define LED_PIN 40
#define RESETPIN 13
//Don't need to edit anything else
int switch_pressed = 0;
int x = 0;
int y = 0;
int z = 0;
int sendAPIpacket = 0;
int firstpacketsend = 0;
int secondpacketsent = 0;
int cookieexist = 0;
int size = 0;
int waitingforcookie = 0;
int clientstopped = 0;
int finlcd = 0;
int returntobaselcd = 0;
//cookiebuffer: one reason why Mega is used.
//someone knows of exapmle how to use flash to store data please tell
String cookiebuffer = "HTTP/1.1 200 OK Date: Wed, 27 Dec 2017 09:06:39 GMT Server: Apache/25555 (GNU/Linux) X-Powered-By: PHP/5lslkslkslkslklkslkslksSet-Cookie: ZMSESSID=ffffffffffffffffffffffff; path=/; HttpOnly Expires: Mon, 26 Jul 1997 05:00:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Set-Cookie: zmSkin=classic; expires=Fri, 05-Nov-2027 09:06:39 GMT; Max-Age=311040000 Set-Cookie: zmCSS=classic; expires=Fri, 05-Nov-2027 09:06:39 GMT; Max-Age=311040000 Set-Cookie: ZMSESSID=ffffffffffffffffffffffffff; path=/; HttpOnly Last-Modified: Wed, 27 Dec 2017 09:06:39 GMT Cache-Control: post-check=0, pre-check=0 Vary: Accept-Encoding ransfer-Encoding: chunked";
String cookie = "startdatastartdatastartdatastartdata";
EthernetClient client;
/*
*
* Basic API Request. See notes in main loop below for more details.
* This passes the host (zm server ip), the cookie, and the API request to Zoneminder
*
*/
void API_Request(String api_req){
client.print("GET ");
client.print(api_req);
client.println(" HTTP/1.1");
client.print("Host: ");
client.println(host);
client.println("Accept: */*");
client.print("Cookie: zmCSS=classic; zmSkin=classic; ");
client.println(cookie);
// ask for a range back. We don't care whats in the response.
// and we need to quickly close this connection so in case of
// a connection fail, we can power off/ try again without error.
// small micros don't have sram to parse the response fast enough
client.println("Range: bytes=0-10");
client.println();
}
void chime(int freq){
tone(SPEAKER_PIN, freq, 50);
delay(50);
}
void chimefast(int freq, int fast){
tone(SPEAKER_PIN, freq, fast);
delay(fast);
}
//timer/interrupt
uint16_t timer1;
uint16_t timer1_counter;
uint8_t debouncetime;
uint8_t first_interrupt = 0;
//timer for debounce
ISR(TIMER1_OVF_vect){
timer1++;
if (first_interrupt == 1 ){
debouncetime++;
}
if (debouncetime > 2) {
first_interrupt = 0;
debouncetime = 0;
}
}
void setup()
{
Serial.begin(9600);
Serial.println("ZoneAVR Switch");
pinMode(SWITCHONE,INPUT_PULLUP);
pinMode(SWITCHTWO,INPUT_PULLUP);
pinMode(SWITCHTHREE,INPUT_PULLUP);
pinMode(SWITCHFOUR,INPUT_PULLUP);
Ethernet.begin(mac, ip);
lcd.begin(8, 2);
lcd.print("ZoneAVR");
lcd.setCursor(0, 1);
lcd.print(" Switch ");
//mega crystal is same as uno, so 16MHz
//unless you change it via new clock or pll
//timer 1, setup
TCCR1A = 0;
TCCR1B = 0;
// Set timer1_counter to the correct value for our interrupt interval
timer1_counter = 10000; //62500 for one second if using 256 prescaler. can't be over 16 bit value (timer1 is 16bit limited)
TCNT1 = timer1_counter; // TCNT1 is what we are overflowing on
//TCCR1B |= (1 << CS12); // 256 prescaler (divide 16mhz/256 = 62500)
TCCR1B |= 00000101; //https://web.archive.org/web/20170707164930/http://www.avrbeginners.net:80/architecture/timers/timers.html
//search tccr1b
TIMSK1 |= (1 << TOIE1); // enable timer overflow interrupt (if goes over timer, interrupt flagged)
//end timer1
sei(); //timer needs interrupts
tone(SPEAKER_PIN, 1000, 200);
delay(100);
tone(SPEAKER_PIN, 2000, 200);
delay(100);
tone(SPEAKER_PIN, 2200, 200);
delay(100);
}
void loop()
{
//check buttons
//timer interrupt, gives two second delay
if (digitalRead(SWITCHONE) == LOW && first_interrupt == 0){
sendAPIpacket=1; //send api packet
firstpacketsend = 1;
switch_pressed = 1;
tone(SPEAKER_PIN, 2400, 150);
delay(150);
tone(SPEAKER_PIN, 3200, 150);
delay(150);
}
if (digitalRead(SWITCHTWO) == LOW && first_interrupt == 0){
sendAPIpacket = 1; //send api packet
firstpacketsend = 1;
switch_pressed = 2;
tone(SPEAKER_PIN, 3000, 150);
delay(150);
tone(SPEAKER_PIN, 3800, 150);
delay(150);
tone(SPEAKER_PIN, 4500, 150);
delay(150);
}
if (digitalRead(SWITCHTHREE) == LOW && first_interrupt == 0){
sendAPIpacket = 1; //send api packet
firstpacketsend = 1;
switch_pressed = 3;
tone(SPEAKER_PIN, 2100, 150);
delay(150);
tone(SPEAKER_PIN, 3500, 150);
delay(150);
tone(SPEAKER_PIN, 3200, 150);
delay(150);
}
if (digitalRead(SWITCHFOUR) == LOW && first_interrupt == 0){
sendAPIpacket = 1; //send api packet
firstpacketsend = 1;
switch_pressed = 4;
tone(SPEAKER_PIN, 2800, 150);
delay(150);
tone(SPEAKER_PIN, 1800, 150);
delay(150);
}
//switch on
if (switch_pressed > 0 && firstpacketsend == 1 && cookieexist == 0 && waitingforcookie == 0) {
Serial.println("switch activated");
firstpacketsend = 0;
cli();
first_interrupt = 1;
debouncetime = 0;
sei();
chime(3200);
Serial.println("connecting...");
lcd.clear();
delay(10);
lcd.print("Switched");
if (client.connect(server, 80)) {
Serial.println("if server connect...");
chime(3300);
//proper usage of post request for zm. (See ZM Documentation on API and curl)
//POST /zm/index.php HTTP/1.1
//User-Agent: curl/7.38.0
//Host: <zmserverip>
//Accept: */*
//Content-Length: 59
//Content-Type: application/x-www-form-urlencoded
//
//username=user&password=pass&action=login&view=console
//
//NOTE: this must be exactly the same, except for user agent (that is optional). Nothing else can be omitted.
//When in doubt, test with curl, and make sure you get the page to load correctly. Not just
//a 200 response, it also needs to 'login' and load the homepage.
//Also helpful to review both tcpdumps (tcpdump -ni eth0 -A -s 1500 > log) of a working and non working req
//contrast and compare carefully
lcd.clear();
delay(10);
lcd.print("Cnnected");
Serial.println("connected");
client.println("POST /zm/index.php HTTP/1.1"); //required
client.print ("Host: "); //required
client.println(host); //required
client.println("Accept: */*"); //i think required
client.println("Content-Length: 59"); //required
//client.println("Range: bytes=0-100"); //doesn't work here
client.println("Content-Type: application/x-www-form-urlencoded"); //required
client.println(); //required
client.print("username=");
client.print(username);
client.print("&password=");
client.print(password);
client.println("&action=login&view=console");
client.println(); //required
waitingforcookie = 1;
}
else {
Serial.println("connection failed");
lcd.clear();
delay(10);
lcd.print("ConnFail");
}
}
//if packets, read packets
//if (client.available()) {
//this may be the wrong approach. see exmaple
//while packets still available
while((size = client.available()) > 0 && waitingforcookie == 1){
chime(3400);
//client.available return value is number of bytes available
//we don't need all the bytes for zoneminder auth. Just begin
for (x=0;x<700;x++){
char c = client.read();
Serial.print(c);
if (x<700){
cookiebuffer[x]=c;
}
//give delay so it loads everything. don't want to read too fast and miss bytes
//required
delay(1);
}
waitingforcookie = 0;
cookieexist = 1; //don't ask again for cookies this session
//don't close the connection yet. this seems to cause tcp retransmissions
//and tcp dup problems
//client.stop();
}
//none of the below strcat works
//http://www.nongnu.org/avr-libc/user-manual/group__avr__string.html
//http://web.archive.org/web/20161224161834/http://www.avrfreaks.net:80/forum/string-manipulation-0
// strcat(cookiebuffer, c);
//disconnect
if (!client.connected()) {
client.stop();
}
if (returntobaselcd == 1){
lcd.clear();
delay(10);
lcd.print("ZoneAVR");
lcd.setCursor(0, 1);
lcd.print(" Switch ");
returntobaselcd = 0;
}
//cookie obtained
if (sendAPIpacket == 1 && cookieexist == 1) {
chime(3500);
//client.stop();
Serial.println();
//debug
//Serial.println(cookiebuffer);
//i don't like these string manipulations. i'd like
//to do this over in plain c/ std libraries with char arrays.
//the Set-Cookie we need is the second ZMSESSID in the return packet from ZM
//first one, is not relevant.
//example of average return packet from ZM:
/*
*
*
HTTP/1.1 200 OK
Date: Fri, 29 Dec 2017 02:59:02 GMT
Server: Apache (GNU/Linux)
X-Powered-By: PHP
Set-Cookie: ZMSESSID=ffffffffffffffffffffffff; path=/; HttpOnly
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: zmSkin=classic; expires=Sun, 07-Nov-2027 02:59:02 GMT; Max-Age=311040000
Set-Cookie: zmCSS=classic; expires=Sun, 07-Nov-2027 02:59:02 GMT; Max-Age=311040000
Set-Cookie: ZMSESSID=lklkslkslklkslklksslk; path=/; HttpOnly
Last-Modified: Fri, 29 Dec 2017 02:59:02 GMT
Cache-Control: post-check=0, pre-check=0
Vary: Accept-Encoding
*
*
*/
z = cookiebuffer.indexOf("zmCSS");
x = cookiebuffer.indexOf("Cookie:", z+24);
y = cookiebuffer.indexOf("path=/;", z+24);
cookie = cookiebuffer.substring(x+8, y);
Serial.println(cookie);
chime(3600);
/*
* Packet to Send to Zoneminder API:
* e.g.
*
* GET /zm/api/states.json HTTP/1.1
* User-Agent: curl/7.35.0
* Host: 127.0.0.1
* Accept: * / *
* Cookie: zmCSS=classic; zmSkin=classic; ZMSESSID=<something>
*
* note that accept needs the space between asterisks and slash removed.
*
*/
//delay(50);
if (client.available() && clientstopped == 0 || client.connect(server,80)){
chime(3700);
Serial.println("Sending packet 2...");
//double check all this: #tcpdump -ni eth0 -A -s 1500 &> log
switch (switch_pressed) {
case 1:
//lcd.print("One");
API_Request(APIREQUEST1);
break;
case 2:
//lcd.print("Two");
API_Request(APIREQUEST2);
break;
case 3:
//lcd.print("Three");
API_Request(APIREQUEST3);
break;
case 4:
//lcd.print("Four");
API_Request(APIREQUEST4);
break;
}
//blink a led
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN,HIGH);
lcd.clear();
delay(5);
lcd.print(".");
//play a tune
tone(SPEAKER_PIN, 1500, 150);
delay(150);
tone(SPEAKER_PIN, 1000, 150);
delay(150);
lcd.print(".");
tone(SPEAKER_PIN, 1200, 150);
delay(150);
tone(SPEAKER_PIN, 1100, 150);
delay(150);
lcd.print(".");
tone(SPEAKER_PIN, 1200, 150);
delay(150);
lcd.print(".");
tone(SPEAKER_PIN, 1000, 150);
delay(150);
tone(SPEAKER_PIN, 900, 150);
delay(150);
lcd.print(".");
tone(SPEAKER_PIN, 1000, 150);
delay(150);
tone(SPEAKER_PIN, 1100, 50);
lcd.print(".");
delay(50);
tone(SPEAKER_PIN, 1200, 50);
delay(50);
tone(SPEAKER_PIN, 1000, 50);
delay(50);
tone(SPEAKER_PIN, 900, 50);
delay(50);
tone(SPEAKER_PIN, 1000, 50);
lcd.print(".");
delay(50);
//room full of tunes
tone(SPEAKER_PIN, 1100, 50);
delay(50);
tone(SPEAKER_PIN, 1200, 50);
delay(50);
tone(SPEAKER_PIN, 1000, 50);
delay(50);
lcd.print(".");
tone(SPEAKER_PIN, 900, 50);
delay(50);
tone(SPEAKER_PIN, 1000, 50);
delay(50);
lcd.clear();
delay(10);
lcd.print("Success");
Serial.println("Req Sent Successfully");
//need time to do everything before reboot
//can cut to 500 or lower, but then doesn't receive
//packet in serial
delay(1000);
sendAPIpacket = 0;
}
else {
Serial.println("connection failed");
lcd.clear();
delay(10);
lcd.print("ConnFail");
//write to eeprom if connection failed, and try again upon reboot
//EEPROM.write(EEPROM_RETRY, switch_pressed);
}
secondpacketsent = 1; //yes, second packet is sent
//everything done. Power off switch.
} //end API req
//not waiting for cookie, so just read
while((size = client.available()) > 0 && cookieexist == 1 && secondpacketsent == 1){
chime(3400);
lcd.clear();
delay(10);
lcd.print(" Read");
for (x=0;x<700;x++){
char c = client.read();
Serial.print(c);
//give delay so it loads everything
//required
lbg.drawValue( x, 700);
delay(1);
}
client.stop();
clientstopped = 1;
lcd.clear();
delay(10);
lcd.print(" Fin");
delay(1000);
returntobaselcd = 1;
}
} //end main loop