"Smart home" with your own hands. Part 4. We organize the web interface

    In a previous article, we were able to teach our smart home system to recognize what we said and synthesize voice responses using Google.
    Today I want to tell how to organize access to our system through the web interface.


    Technology


    As you remember, we write perl software for managing our “smart home” . A modern information system is almost unthinkable without a database. We, too, will not stand aside and will use MySQL DBMS to store our data . To implement the web server, I decided not to use third-party software, but the module for perl - HTTP :: Server :: Simple , in particular - HTTP :: Server :: Simple :: CGI . Why did I do this? For the most part, for the sake of interest;) But in theory, you can access the low-level processing of HTTP requests / responses without piling up the Apache / mod_perl complex. In general, nothing prevents you from moving the project to Apache if you have the desire and enough time.

    Database


    First, install the MySQL DBMS and create a database with tables from db.sql. Here is the listing:

    CREATEDATABASE ion;
    USE ion;
    ## Table structure for table 'calendar'#DROPTABLEIFEXISTS calendar;
    CREATETABLE`calendar` (
      `id`int(15) NOTNULL AUTO_INCREMENT,
      `date` datetime NOTNULL,
      `message`text,
      `nexttimeplay` datetime NOTNULL,
      `expired` datetime NOTNULL,
      `type`int(1) DEFAULTNULL,
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM DEFAULTCHARSET=latin1;
    ## Table structure for table 'commandslog'#DROPTABLEIFEXISTS commandslog;
    CREATETABLE`commandslog` (
      `id`int(15) NOTNULL AUTO_INCREMENT,
      `date` datetime NOTNULL,
      `cmd`varchar(255) NOTNULL,
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=1DEFAULTCHARSET=latin1;
    ## Table structure for table 'log'#DROPTABLEIFEXISTSlog;
    CREATETABLE`log` (
      `id`int(15) NOTNULL AUTO_INCREMENT,
      `date` datetime NOTNULL,
      `message`varchar(255) NOTNULL,
      `level`int(1) DEFAULTNULL,
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=1DEFAULTCHARSET=latin1;
    


    We perform the necessary actions:

    nix@nix-boss:~$ sudo apt-get install mysql-server
    nix@nix-boss:~$ mysql -uroot -ppassword < db.sql

    Modify the code


    Now we need to create the lib , html and config folders (next to the data folder ). In the lib folder, we put the module responsible for implementing the web server and processing our HTTP requests.

    We need to tweak the srv.pl script a bit . Add to the initialization block:

    our %cfg = readCfg("common.cfg");
    our $dbh = dbConnect($cfg{'dbName'}, $cfg{'dbUser'}, $cfg{'dbPass'});
    

    Add the lines responsible for starting the HTTP server below the initialization block:

    ## Запуск HTTP-сервера################################my $pid = lib::HTTP->new($cfg{'httpPort'})->background();
    print"HTTP PID: $pid\n";
    logSystem("Сервис HTTP - PID: $pid, порт: $cfg{'httpPort'}, хост: $cfg{'httpHost'}", 0);
    ################################

    Now add the missing functions to the end of the file:

    subreadCfg{
      my $file = shift;
      my %cfg;
      open(CFG, "<config/$file") || die $!;
        my @cfg = <CFG>;
        foreachmy $line (@cfg)
        {
          nextif $line =~ /^\#/;
          if ($line =~ /(.*?) \= \"(.*?)\"\;/)
          {
    	chomp $2;
    	$cfg{$1} = $2;
          }
        }
      close(CFG);
      return %cfg;
    }
    ########################################subdbConnect{
     my ($db, $user, $pass) = @_;
     return $dbh = DBI->connect("DBI:mysql:$db", $user, $pass) || die"Could not connect to database: $DBI::errstr";
    }
    ########################################sublogSystem{
     my ($text, $level) = @_;
     my %cfg = readCfg("common.cfg");
     dbConnect($cfg{'dbName'}, $cfg{'dbUser'}, $cfg{'dbPass'});
     $dbh->do("INSERT INTO log (date, message, level) VALUES (NOW(), '$text', $level)");
    }
    


    As you can see by the names of the functions, dbConnect () is responsible for connecting to our DBMS, logSystem () for logging, readCfg () for loading the configuration. Let us dwell on it in more detail. The configuration is a simple text file in the config directory. In our case, it is called common.cfg . It looks something like this:

    ## НастройкиdaemonMode = "undef";
    logSystem = "1";
    logUser = "1";
    dbName = "ion";
    dbUser = "root";
    dbPass = "password";
    camNumber = "4";
    camMotionDetect = "1"; 
    httpPort = "16100";
    httpHost = "localhost";
    telnetPort = "16000";
    telnetHost = "localhost";
    micThreads = "5";
    


    Some lines in it will be used later. So far, we are only interested in lines starting with the db prefix . As we see, these are the settings for connecting to our database.

    Now I’ll talk about how to overcome multiple command execution. Edit the function checkcmd () :

    subcheckcmd{
    	my $text = shift;
    	chomp $text;
    	$text =~ s/ $//g;
    	print"+OK - Got command \"$text\" (Length: ".length($text).")\n";
    	if($text =~ /система/)
    	{
    	#################################################my $sth = $dbh->prepare('SELECT cmd FROM commandslog WHERE DATE_SUB(NOW(),INTERVAL 4 SECOND) <= date LIMIT 0, 1');
    	$sth->execute();
    	my $result = $sth->fetchrow_hashref();
    	if($result->{cmd} ne"") { return; }
    	$dbh->do("INSERT INTO commandslog (date, cmd) VALUES (NOW(), '$text')");
    	#################################################if($text =~ /провер/) { my $up = `uptime`; $up =~ /up (.*?),/; sayText("Время работы сервера - $1. Номер главного процесса - $parent."); }
    	  if($text =~ /врем/) { my $up = `uptime`; $up =~ /(.*?) up/;  sayText("Сейчас $1"); }
    	  if($text =~ /законч/ || $text =~ /заверш/) { sayText("Завершаю работу. Всего доброго!"); system("killall motion"); system("rm ./data/*.flac && rm ./data/*.wav"); system("killall perl"); exit(0); }
    	  if($text =~ /погод/)
    	      {
    		  my ($addit, $mod);
    		  my %wh = lib::HTTP::checkWeather();
    		  $wh{'condition'} = Encode::decode_utf8( $wh{'condition'}, $Encode::FB_DEFAULT );
    		  $wh{'hum'} = Encode::decode_utf8( $wh{'hum'}, $Encode::FB_DEFAULT );
    		  $wh{'wind'} = Encode::decode_utf8( $wh{'wind'}, $Encode::FB_DEFAULT );
    		  if($wh{'temp'} < 0) { $mod = "ниже нуля"; }
    		  if($wh{'temp'} > 0) { $mod = "выше нуля"; }
    		  $wh{'wind'} =~ s/: В,/восточный/; $wh{'wind'} =~ s/: З,/западный/; $wh{'wind'} =~ s/: Ю,/южный/; $wh{'wind'} =~ s/: С,/северный/;
    		  $wh{'wind'} =~ s/: СВ,/северо-восточный/; $wh{'wind'} =~ s/: СЗ,/северо-западный/; $wh{'wind'} =~ s/: ЮВ,/юго-восточный/; $wh{'wind'} =~ s/: ЮЗ,/юго-западный/;
    		  sayText("Сейчас $wh{'condition'}, $wh{'temp'} градусов $mod. $wh{'hum'}. $wh{'wind'}");
    		  if ($wh{'temp'} <= 18) { $addit = sayText("Одевайтесь теплее, на улице холодно!"); }
    		  if ($wh{'temp'} >= 28) { $addit = sayText("Переносной кондиционер не помешает!"); }
    	      }
    	}
    	#sayText("Ваша команда - $text");return;
    }
    

    We select the last command executed in the interval of four seconds and if it coincides with the current one, we exit the function. As you can see, I added some commands, compared to the described function in the last article. The most interesting is the weather. The implementation of obtaining data for her is slightly lower.

    HTTP.pm module


    Back to the implementation of the embedded HTTP server. Create the HTTP.pm file in the lib directory . We write the following code there:

    package lib::HTTP;
    use HTTP::Server::Simple::CGI;
    use LWP::UserAgent;
    use URI::Escape;
    use base qw(HTTP::Server::Simple::CGI);
    use Template;
    ##################################################################################our %dispatch = (
         '/' => \&goIndex,
         '/index' => \&goIndex,
         '/camers' => \&goCamers,
    );
    our $tt = Template->new();
    ##################################################################################subhandle_request{
         my $self = shift;
         my $cgi  = shift;
         my $path = $cgi->path_info();
         my $handler = $dispatch{$path};
         if ($path =~ qr{^/(.*\.(?:png|gif|jpg|css|xml|swf))})
    	{
    		my $url = $1;
    		print"HTTP/1.0 200 OK\n";
    		print"Content-Type: text/css\r\n\n"if $url =~ /css/;
    		print"Content-Type: image/jpeg\r\n\n"if $url =~ /jpg/;
    		print"Content-Type: image/png\r\n\n"if $url =~ /png/;
    		print"Content-Type: image/gif\r\n\n"if $url =~ /gif/;
    		print"Content-Type: text/xml\r\n\n"if $url =~ /xml/;
    		print"Content-Type: application/x-shockwave-flash\r\n\n"if $url =~ /swf/;
    		open(DTA, "<$url") || die"ERROR: $! - $url";
    		   binmode DTA if $url =~ /jpg|gif|png|swf/;
    		   my @dtast = <DTA>;
    		   foreachmy $line (@dtast) { print $line; }
    		close(DTA);
    		return;
    	}
         if (ref($handler) eq "CODE") {
             print"HTTP/1.0 200 OK\r\n";
             $handler->($cgi);
         } else {
             print"HTTP/1.0 404 Not found\r\n";
             print $cgi->header,
                   $cgi->start_html('Not found'),
                   $cgi->h1('Not found'),
    	       $cgi->h2($cgi->path_info());
                   $cgi->end_html;
         }
    }
    ## Обработка запроса /########################################subgoIndex{
         my $cgi  = shift;   # CGI.pm objectreturnif !ref $cgi;
         my %w = checkWeather();
         my $cmd;
    	my $dbh = iON::dbConnect($iON::cfg{'dbName'}, $iON::cfg{'dbUser'}, $iON::cfg{'dbPass'});
     	my $sth = $dbh->prepare('SELECT cmd FROM commandslog WHERE id > 0 ORDER BY id DESC LIMIT 0, 1');
    	$sth->execute();
    	my $result = $sth->fetchrow_hashref();
    	if($result->{cmd} ne"") { $cmd = $result->{cmd}; } else { $cmd = "Нет комманд..."; }
         print"Content-Type: text/html; charset=UTF-8\n\n";
          my $uptime = `uptime`;
    	 $uptime =~ /up (.*?),/;
    	 $uptime = $1;
          my $videosys = `ps aux | grep motion`;
    	 if ($videosys =~ /motion -c/) { $videosys = "<font color=green>работает</font>"; } else { $videosys = "<font color=red>не работает</font>"; }
          my $micsys = `ps aux | grep mic`;
    	 if ($micsys =~ /perl mic\.pl/) { $micsys = "<font color=green>работает</font>"; } else { $micsys = "<font color=red>не работает</font>"; }
    	my $vars = {
    	    whIcon => $w{'icon'},
    	    whCond => $w{'condition'},
    	    whTemp => $w{'temp'},
    	    whHum => $w{'hum'},
    	    whWind => $w{'wind'},
    	    cmd => $cmd,
    	    uptime => $uptime,
    	    video => $videosys,
    	    mic => $micsys,
    	    threads => $iON::cfg{'micThreads'},
    	};
         my $output;
         $tt->process('html/index', $vars, $output) || print $tt->error(), "\n";
    }
    ## Обработка запроса /camers########################################subgoCamers{
         my $cgi  = shift;   # CGI.pm objectreturnif !ref $cgi;
         my %w = checkWeather();
         my $cmd;
    	my $dbh = iON::dbConnect($iON::cfg{'dbName'}, $iON::cfg{'dbUser'}, $iON::cfg{'dbPass'});
     	my $sth = $dbh->prepare('SELECT cmd FROM commandslog WHERE id > 0 ORDER BY id DESC LIMIT 0, 1');
    	$sth->execute();
    	my $result = $sth->fetchrow_hashref();
    	if($result->{cmd} ne"") { $cmd = $result->{cmd}; } else { $cmd = "Нет комманд..."; }
         if($cgi->param("text") ne"")
          {
    	my $txt = $cgi->param('text');
    	require Encode;
    	$txt = Encode::decode_utf8( $txt, $Encode::FB_DEFAULT );
    	iON::sayText($txt);
          }
         print"Content-Type: text/html; charset=UTF-8\n\n";
    	my $vars = {
    	    camera1 =>'video-0/camera.jpg',
                camera2 =>'video-1/camera.jpg',
                camera3 =>'video-2/camera.jpg',
                camera4 =>'video-3/camera.jpg',
    	    whIcon => $w{'icon'},
    	    whCond => $w{'condition'},
    	    whTemp => $w{'temp'},
    	    whHum => $w{'hum'},
    	    whWind => $w{'wind'},
    	    cmd => $cmd,
    	};
         my $output;
         $tt->process('html/camers', $vars, $output) || print $tt->error(), "\n";
    }
    ## Погода########################################subcheckWeather{
      my %wh;
      my $ua = LWP::UserAgent->new(
    		agent =>"Mozilla/5.0 (Windows NT 5.1; ru-RU) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.872.0 Safari/535.2");
      my $content = $ua->get("http://www.google.com/ig/api?hl=ru&weather=".uri_escape("Санкт-Петербург"));
         $content->content =~ /<current_conditions>(.*?)<\/current_conditions>/g;
      my $cond = $1;
      $cond =~ /<condition data="(.*?)"/g;
      $wh{'condition'} = $1;
      $cond =~ /temp_c data="(.*?)"/g;
      $wh{'temp'} = $1;
      $cond =~ /humidity data="(.*?)"/g;
      $wh{'hum'} = $1;
      $cond =~ /icon data="(.*?)"/g;
      $wh{'icon'} = $1;
      $cond =~ /wind_condition data="(.*?)"/g;
      $wh{'wind'} = $1;
     return %wh;
    }
    ##################################################################################1;
    


    Let's analyze the contents in more detail. In the % dispatch hash, we match the URL and the function being called. All other URLs not described in this hash will return a 404 page .
    A powerful and flexible Template Toolkit library will be our template engine . We initialize it with the line:

    our $tt = Template->new();
    

    Overloading the handle_request () function of the parent class, we get control over the processing of requests to the HTTP server. To give the browser static content (png, gif, jpg, css, xml, swf), use the block:

    if ($path =~ qr{^/(.*\.(?:png|gif|jpg|css|xml|swf))})
    	{
    		my $url = $1;
    		print"HTTP/1.0 200 OK\n";
    		print"Content-Type: text/css\r\n\n"if $url =~ /css/;
    		print"Content-Type: image/jpeg\r\n\n"if $url =~ /jpg/;
    		print"Content-Type: image/png\r\n\n"if $url =~ /png/;
    		print"Content-Type: image/gif\r\n\n"if $url =~ /gif/;
    		print"Content-Type: text/xml\r\n\n"if $url =~ /xml/;
    		print"Content-Type: application/x-shockwave-flash\r\n\n"if $url =~ /swf/;
    		open(DTA, "<$url") || die"ERROR: $! - $url";
    		   binmode DTA if $url =~ /jpg|gif|png|swf/;
    		   my @dtast = <DTA>;
    		   foreachmy $line (@dtast) { print $line; }
    		close(DTA);
    		return;
    	}
    

    Since I didn’t get a lot of MIME types, I wrote them a little Hindu;)
    Then the functions responsible for generating the content of a specific URL begin. So far there are only two of them - an index and a page with cameras.
    On the index, we can see if subsystems such as video and audio capture work. A separate line is:

    my %w = checkWeather();
    

    This function returns a hash with the current weather data in the city, which will be displayed on our page. Such a small pleasant bun;)
    In the same place, we will output the last received and recognized command for the “smart home”.

    The next function goCamers () performs the same functions as the index, but instead of displaying information on the status of the subsystems, it shows an image from our cameras and it is possible to write some text that will be synthesized and voiced by our “smart home”.

    All pages are based on templates in the html folder . It will not be convenient to upload a listing here, so I will give a link to the archive - html.zip .

    Total


    After all the changes, we will get a well-functioning web interface on port 16100 , which will now allow you to view the status of the “smart home” subsystems, data from webcams, and voice the text you entered. In the future, we will add another, more important functionality to it.

    In the next article I will talk about how to work with X10 devices and integrate them into our smart home system.

    upd : Continued

    Also popular now: