📜 ⬆️ ⬇️

"Smart home" with their own hands. Part 4. We organize a web interface

In the 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 you how to organize access to our system through a web interface.


Technology


As you remember, the software for managing our “smart home” is written in the Perl language. A modern information system is practically unthinkable without a database. We also will not stay aside and will use MySQL for the storage of our data. To implement the web server, I decided to use not a third-party software, but a module for perl - HTTP :: Server :: Simple , in particular - HTTP :: Server :: Simple :: CGI . Why did I do this? In large part, for the sake of interest;) But in theory, you can get access to low-level processing of HTTP requests / responses without cluttering up the Apache / mod_perl complex. In general, nothing prevents the project from being transferred to Apache rails 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:
')
CREATE DATABASE ion; USE ion; # # Table structure for table 'calendar' # DROP TABLE IF EXISTS calendar; CREATE TABLE `calendar` ( `id` int(15) NOT NULL AUTO_INCREMENT, `date` datetime NOT NULL, `message` text, `nexttimeplay` datetime NOT NULL, `expired` datetime NOT NULL, `type` int(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; # # Table structure for table 'commandslog' # DROP TABLE IF EXISTS commandslog; CREATE TABLE `commandslog` ( `id` int(15) NOT NULL AUTO_INCREMENT, `date` datetime NOT NULL, `cmd` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; # # Table structure for table 'log' # DROP TABLE IF EXISTS log; CREATE TABLE `log` ( `id` int(15) NOT NULL AUTO_INCREMENT, `date` datetime NOT NULL, `message` varchar(255) NOT NULL, `level` int(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; 


Perform the necessary actions:

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

Modifying 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); ################################ 

And now add the missing functions to the end of the file:

 sub readCfg { my $file = shift; my %cfg; open(CFG, "<config/$file") || die $!; my @cfg = <CFG>; foreach my $line (@cfg) { next if $line =~ /^\#/; if ($line =~ /(.*?) \= \"(.*?)\"\;/) { chomp $2; $cfg{$1} = $2; } } close(CFG); return %cfg; } ######################################## sub dbConnect { my ($db, $user, $pass) = @_; return $dbh = DBI->connect("DBI:mysql:$db", $user, $pass) || die "Could not connect to database: $DBI::errstr"; } ######################################## sub logSystem { 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 can be understood by the function names, dbConnect () is responsible for connecting to our DBMS, logSystem () is for logging, readCfg () is 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 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. We are still interested only in lines beginning with the prefix db . As we can see, these are the settings for connecting to our database.

Now I will tell you how to overcome the repeated execution of a command. Edit the checkcmd () function:

 sub checkcmd { 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 function described in the last article. The most interesting is the weather. The implementation of receiving data for it is slightly lower.

HTTP.pm module


Let's go back to the implementation of the embedded HTTP server. Create an 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(); ######################################### ######################################### sub handle_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>; foreach my $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; } } ##   / ######################################## sub goIndex { my $cgi = shift; # CGI.pm object return if !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 ######################################## sub goCamers { my $cgi = shift; # CGI.pm object return if !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"; } ##  ######################################## sub checkWeather { 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; 


We analyze the content in more detail. In the % dispatch hash, we determine the correspondence between the URL and the function being called. All other URLs not described in this hash will display a 404 page.
The template engine will be a powerful and flexible Template Toolkit library. We initialize it with the string:

 our $tt = Template->new(); 

By overloading the handle_request () function of the parent class, we get control over the processing of requests to the HTTP server. To return static content to the browser (png, gif, jpg, css, xml, swf) a block is used:

  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>; foreach my $line (@dtast) { print $line; } close(DTA); return; } 

Since I got a bit of MIME types, I wrote them down a little Hindu;)
Then begin the functions responsible for generating the content of a specific URL. So far there are only two - an index and a camera page.
On the index, we can see whether subsystems such as video and audio capture work. A separate line is:

 my %w = checkWeather(); 

This function returns a hash with current city weather data that will be displayed on our page. Such a nice little bun;)
In the same place next to we will display the last received and recognized command for the “smart home”.

The next function goCamers () performs the same functions as the index, only instead of displaying information on the state 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 that are in the html folder. Lay out the listing here is not convenient, so I will give a link to the archive - html.zip .

Total


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

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

upd : Continued

Source: https://habr.com/ru/post/130239/


All Articles