zoneminder-controlcenter: console interface to set ZoneMinder run state

If you've made a patch to quick fix a bug or to add a new feature not yet in the main tree then post it here so others can try it out.
Post Reply
montagdude
Posts: 88
Joined: Fri Nov 10, 2017 6:05 pm

zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by montagdude »

I made a simple text UI to set the ZoneMinder run state. It is intended to be run on a small computer like a Raspberry Pi with a monitor and keypad. It uses the ZoneMinder API and requires the user to match a PIN in order to change the run state. Maybe something like this already exists, but it was easy and fun to make.

https://github.com/montagdude/zoneminder-controlcenter

Usage:

Code: Select all

./zm_controlcenter SERVER
where SERVER is the address of your ZoneMinder server, e.g. http://server_ip. You will be prompted for the username, password, and PIN when it starts up. This information can be optionally specified on the command line instead.

Image

Image

I'm currently working on my console setup. I have a Zotac ZBox mini computer that was previously my ZoneMinder server, and I purchased a 5-inch screen and keypad. Now I just need to 3D print a wall mount. I'll post a picture when that's done. In the meantime, it's already working, just with the monitor and keypad sitting on a counter. Let me know if you try this out and what you think of it.
Magic919
Posts: 1381
Joined: Wed Sep 18, 2013 6:56 am

Re: zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by Magic919 »

You should stick this on the Slack channel.
-
montagdude
Posts: 88
Joined: Fri Nov 10, 2017 6:05 pm

Re: zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by montagdude »

Magic919 wrote: Fri Jan 20, 2023 8:16 am You should stick this on the Slack channel.
Never used Slack before. How does it work? I assume I have to make an account and then request access to the Slack channel or something like that?
montagdude
Posts: 88
Joined: Fri Nov 10, 2017 6:05 pm

Re: zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by montagdude »

I finished my 3D printed wall mount today. It's a little big because of the size of the keypad I chose, but it gets the job done and looks pretty good.

Image
User avatar
burger
Posts: 390
Joined: Mon May 11, 2020 4:32 pm

Re: zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by burger »

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

fastest way to test streams:
ffmpeg -i rtsp://<user>:<pass>@<ipaddress>:554/path ./output.mp4 (if terminal only)
ffplay rtsp://<user>:<pass>@<ipaddress>:554/path (gui)
find paths on ispydb or in zm hcl

If you are new to security software, read:
https://wiki.zoneminder.com/Dummies_Guide
Magic919
Posts: 1381
Joined: Wed Sep 18, 2013 6:56 am

Re: zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by Magic919 »

Slack link here https://zoneminder.com/ You just need to create the account. You can just use the web version to save installation of Slack app.
-
montagdude
Posts: 88
Joined: Fri Nov 10, 2017 6:05 pm

Re: zoneminder-controlcenter: console interface to set ZoneMinder run state

Post by montagdude »

Thanks.
Post Reply