16 mayo 2011

Mi pequeño Skynet

No dejéis que el avatar os confunda! no voy a hablar hoy de la adquisición de Skype por parte de la poderosa Microsoft y que eso de las videoconferencias gratuitas tengan los días contados.

Hace poco leía que, según las películas de Terminator que empezábamos a ver a mediados de los 80 y 90, Skynet, el ordenador inteligente que tomaría conciencia por sí mismo y tendría como principal objetivo el aniquilar la raza humana, el 19 de Abril de 2011.

No, tampoco voy a hablar de ciencia ficción, sino de un tema que es real, que afortunadamente nos hace la vida más fácil y que prometí además hacer en un artículo anterior: la Domótica. El título del post es por las bromas y comentarios que me han hecho varios amigos, con eso de que llevo la domótica un punto más allá del Do It Yourself y que un día de estos la Roomba se haría fuerte en casa y no me dejaría entrar y/o salir.

Por introducir un poco con qué material inicial cuento: 
  • Una cámara wireless y una con cable USB
  • Un voice módem externo Dynalink V1433vqe conectable al puerto serie (cortesía del trastero de mi amigo Domingo)
  • Un sistema de aire acondicionado/calefacción Airzone, con termostatos en 4 habitaciones. Compré una plaquita con un servidor web (denominado Innobus) que permite monitorizar y cambiar, vía web, la temperatura y modo de funcionamiento de los termostatos. El servidor web en mi caso NO está abierto a internet, sino, simplemente para la red local cableada, que no quiero que me pase como al de Samsung, entre otros, y crea que mi casa está construida encima de un cementerio indio cuando los termostatos se vuelvan locos.
  • Una aspiradora Roomba 555 (controlable por Bluetooth)
  • Una alarma de Securitas con un mando a distancia por radiofrecuencia, con conexión al puerto paralelo y al USB
Además, el PC/servidor que tiene conectividad con todo lo anterior, dispone de dos "dongles" bluetooth para interactuar con el resto de los dispositivos.

La idea principal es hacer un sistema de control de presencia (basado fundamentalmente en el BD_Address del bluetooth del móvil de los habitantes de casa) que reaccione de forma automática cuando se dan dos eventos fundamentalmente: cuando no hay nadie en casa; y cuando entramos o salimos de ella.

Así pues, el cron de un usuario no-privilegiado de la máquina que interactúa con todo esto, tiene una pinta tal que así:

*/1 * * * * /usr/bin/perl /usr/local/bin/bt-motion.pl 2> /dev/null > /dev/null
*/1 * * * * /usr/bin/perl /usr/local/bin/roombea.pl 2> /dev/null > /dev/null
*/1 * * * * /usr/bin/perl /usr/local/bin/myscheduler.pl 2> /dev/null > /dev/null

Como se puede ver, cada minuto, "la casa" hace unas cuantas cosas:


bt-motion.pl 

El script bt-motion.pl comprueba si hay alguien en casa o no, y dependiendo de si antes había alguien o no, reacciona encendiendo/apagando Motion (y por ende las cámaras), así como un sistema de contestador automático hecho con el módem externo. Además escribe en un fichero la fecha/hora y si alguien ha llegado o se ha ido de casa.
Os pongo los comentarios dentro del propio código:

[bot@Carmen ~]# more /usr/local/bin/bt-motion.pl
#!/usr/bin/perl

use DBI;
use IPC::Open2;

#VARS
my $dsn = 'DBI:mysql:home:localhost';
my $db_user_name = 'eluserquesea';
my $db_password = 'lapassdeeseuser';

my $BTINFO="/opt/roombafiles/btmotion.info";
my $hci0token="/opt/bluetooth/hci0token";
my $hci1token="/opt/bluetooth/hci1token";
my $firstminute="/var/run/firstminute";
my $pidfile="/var/run/bt-motion.pid";
my $pidttyfile="/var/run/pidttys0.pid";

#ROUTINES
sub alguien_en_casa
{#Función que determina si hay o no alguien en casa en este momento. 
    my $hci_usar, $hcitok; #Al tener la máquina dos dongles Bluetooth hay que ver cual de los dos está libre
    foreach (@BD_Address)  
    {#Para todas las BD_Address permitidas
        if (-e $hci0token)
        {
            if (-e $hci1token)
            {
                $hci_usar=0; #No hay ningún dongle libre
            }
            else 
            {#usar el hci1
                system "touch $hci1token";
                $hci_usar="hci1";
                $hcitok=$hci1token;
            }
        }
        else
        {#usar el hci0
            system "touch $hci0token";
            $hci_usar="hci0";
            $hcitok=$hci0token;
        }
        
        if ($hci_usar ne 0)
        {#Una vez identificado el dongle a usar, busco cada BD_Address    
            my $vuelta=`hcitool -i $hci_usar name $_->[0]`;
            unlink ($hcitok);
            if ($vuelta =~ m/$_->[1]/)
            {#alguien autorizado hay
                   return (1);
            }
        }
    }
    return (0);
}


#MAIN


($sec, $min, $hour, $mday, $mon, $year) = localtime(time()); #fecha/hora
my $datetime= sprintf('%d-%02d-%02d %02d:%02d:%02d:0000', $year + 1900, $mon + 1, $mday, $hour,$min, $sec);

my $dbh = DBI->connect($dsn, $db_user_name, $db_password);

my $sth=$dbh->prepare("select bt,name from allowed_bt");
$sth->execute();


@BD_Address; #BD Addresses permitidas
while (my ($bt,$name)=$sth->fetchrow_array())
{
    push (@BD_Address,[$bt,$name]);
}

my $habia=alguien_en_casa;

if ($habia == 0)
{#no hay nadie
    unless (-e "/var/run/motion.pid") 
    {#Si no estaba encendido, es porque acabamos de salir
        if (-e $firstminute)
        {
            unlink $firstminute;#lo borro
            system "/etc/init.d/motion start"; #Enciendo motion
            system ("/usr/local/bin/notifier.pl 1 \"$logentry Nadie en casa, arranco motion\"");  #Aviso
            open FICH, ">$BTINFO"; 
            print FICH "$datetime\nout\n"; #Guardo hora de salida e indico "out"
            close FICH;
            system "/usr/local/bin/encender_alarma.pl"; #Encendemos la alarma de securitas
            my $salida=`ps -ef|grep -i vgetty|grep -v grep`;
            unless($salida ne "")
            {#Si no lo estaba ya, inicio el contestador automático
                `/sbin/vgetty ttyS0&`;
            }  
        }
        else
        {#Creo el fichero "$firstminute" para que evitar posibles falsos positivos en un único minuto
            system "touch $firstminute";
        }
    }
    else
    {#no hay nadie y esta encendido, copia los ultimos snapshots para poder verlos desde Internet
        system ("/bin/cp /var/motion/usb/lastsnap.jpg /var/www/html/rutasecretadelcopon/snapshotusb.jpg");
        system ("/bin/cp /var/motion/terraza/lastsnap.jpg /var/www/html/rutasecretadelcopon/snapshotterraza.jpg");
        my $salida=`ps -ef|grep -i vgetty|grep -v grep`;
        unless($salida ne "")
        {
            `/sbin/vgetty ttyS0&`;
        }
    }
}
else
{#hay alguien
       if (-e "/var/run/motion.pid")
       {#Si estaba encendido
              system "/etc/init.d/motion stop";#Apago Motion
              system ("/usr/local/bin/notifier.pl 1 \"$logentry Alguien ya en casa, paro motion\""); 
              open FILE, ">$BTINFO";
              print FILE "$datetime\nin\n";#Guardo hora de salida e indico "in"
              close FILE;    
              open (PS, 'ps xu|') or die "$!";
              my @processes = <ps>;
              close PS;
              foreach (@processes)
              {
                   if ($_ =~ m/vgetty/)
                   {
                     @cachos= split(/ /,$_);
                     my $thepid;
                     if (!@cachos[5]) 
                     {
                            $thepid=@cachos[6];        
                     }
                     else 
                     {
                            $thepid=@cachos[5];
                     }
                     kill 9,$thepid;#Apago el contestador
                   }
               }
               unlink $pidttyfile;
        }
        unlink $firstminute;
}


Roombea.pl

El script Roombea.pl, es el encargado de interactuar con la aspiradora Roomba. Lo primero que tiene en cuenta es el fichero de entrada/salida generado por bt-motion.pl. Si detecta que no estamos en casa, al menos durante 15 minutos (si bajo al garage o al trastero no suele ser por mucho más tiempo), comprobará si estamos en lo que se considera "Horario de Limpieza" (de 9:30 AM a 20:00 PM entre semana y de 11:30 a 21:00 PM en fin de semana), y si aún no ha dado la orden de limpiar a la Roomba, y la carga de la batería es mayor del 90%, entonces ordenamos al robot que se ponga a limpiar. Mientras tanto, cada minuto, monitorizaremos la temperatura de la misma y en caso de que la batería supere un determinado valor, la casa me lo notificará con otro script que me envía un correo o un mensaje al MSN/Gtalk, y al Prowl del iPhone o Twitter si el evento es grave. En caso de que roombea.pl detecte (mediante el fichero entrada/salida) que alguien permitido ha llegado a casa, si la roomba aún estaba limpiando, le enviará la señal Dock indicando que vaya a su base (no os imagináis el ruido que mete mientras hace sus labores). De momento, no compruebo cuánto rato ha estado la roomba limpiando, para poder controlar si está menos de un tiempo determinado, si detecta la casa que el ciclo de limpieza ha sido muy corto, poder sacar de nuevo a la Roomba a pasear. Lo tengo en mi To Do para cuando tenga tiempo!

[bot@Carmen ~]# more  /usr/local/bin/roombea.pl 
#!/usr/bin/perl

use IPC::Open2;
use Time::HiRes qw (usleep);
use DateTime;
use Unix::Syslog qw(:macros);  # Syslog macros
use Unix::Syslog qw(:subs);    # Syslog functions


#VARS
my $segundos=900; # 15 minutos/ 900 segs. Tiempo en el que puedo poner a limpiar la Roomba si no hay nadie
my $margen_dock=120; #Check que esta yendose a cargar y no se ha quedado "tirada"
my $BTINFO="/opt/roombafiles/btmotion.info"; 
my $rangobattpermitido="0.9"; #Si tengo que sacar a la roomba de nuevo, al menos que la batería esté al 90% de carga 
my $maxtemp="43"; #Temperatura de batería a partir de la que me avisaré  
my $ROOMBAPID="/opt/roombafiles/roomba.pid";
my $tokenroomba="/opt/roombafiles/tokenroomba.lock";
my $pid;
my $history="/opt/roombafiles/history.txt";
my $pidfil="/opt/roombafiles/roombearun.pid";

#ROUTINES

sub mas_de_x
{
    my $fecha=$_[0];
    my ($sec, $min, $hour, $mday, $mon, $year) = localtime(time()- $_[1]); #fecha - x
    $fecha .= ":0000";
    my $datestring_menos_x = sprintf('%d-%02d-%02d %02d:%02d:%02d:0000', $year + 1900, $mon + 1, $mday, $hour, $min, $sec);
    return ($fecha lt $datestring_menos_x);
}


sub get_roomba_status
{#Devuelve un array con información de los sensores de la Roomba
    my $hci0token="/opt/bluetooth/hci0token";
    my $hci1token="/opt/bluetooth/hci1token";
    my $hci_usar, $hcitok;
    
    while (-e $tokenroomba) {sleep 1;} #Si otro proceso está hablando a la roomba, espero

    if (-e $hci0token)# Decido con qué dongle bluetooth hablaré a la Roomba
    {
        if (-e $hci1token)
        {
            $hci_usar=0;
        }
        else 
        {
            system "touch $hci1token";
            $hci_usar="hci1";
            $hcitok=$hci1token;
        }
    }
    else
    {
        system "touch $hci0token";
        $hci_usar="hci0";
        $hcitok=$hci0token;
    }

    if ($hci_usar ne 0)
    {    
        system "touch $tokenroomba";#Hago un lock a $tokenroomba
        $port="/dev/rfcomm3";
        my $rfcomm="rfcomm -i $hci_usar connect 3 00:06:66:XX:YY:ZZ ";#Conectandome al bluetooth de la Roomba

        sleep (5);

        $pid=0;
        unless (-c $port)
        {
            unlink $port;
            $pid=open2 (\*READ,\*WRITE,$rfcomm);
            sleep (10);
        }

        system("stty -F $port 115200 raw -parenb -parodd cs8 -hupcl -cstopb clocal");#Configurando el puerto 

        my $roomba;
        open $roomba, "+>$port" or die "No he podido abrir el puerto: $!";
        select $roomba; $| =1;  # Vaciando el buffer
   
        printf $roomba "%c",128;  usleep (200); # Mando comando START inicial
        printf $roomba "%c%c",142,6;  usleep (200);  #Gimme your sensors
        for ($i=0; $i<52; $i++)
        {
            $k=getc($roomba);
            $ok=ord ($k);
            push (@roombastatus,$ok);
        }
        unlink $tokenroomba;#Libero el lock de hablarle a la Roomba
        unlink $hcitok;#Libero el dongle bluetooth usado
    }
    return (@roombastatus);
}


#MAIN

unless (-e $pidfil)
{#Si no está arrancado otro proceso Roombea.pl
        #Creacion de pidfile
        open (pidfil,">$pidfil");
        print pidfil "$$\n1";
        close (pidfil);
}
else
{#Si ya estaba
        open (pidfil,"<$pidfil");
        my $pidd=<pidfil>;#Leo el pid que tenía el proceso anterior
        chomp ($pidd);
        my $status=<pidfil>;
        close (pidfil);
        if ($status eq 1)
        {#Lleva un ciclo, vamos a darle otro rato por si acaso
                open (pidfil,">$pidfil");
                print pidfil "$pidd\n2";
                close (pidfil);
                exit (1);
        }
        elsif ($status eq 2)
        {#Se ha quedado pillado, mato el proceso anterior y dejo limpio para la siguiente conexión
                kill 9,$pidd;
                unlink $pidfil;
                exit (2);
        }
}



my @roombastatus;
#Leo del fichero $BTINFO para ver si ha habido algún cambio
open FILE, "<$BTINFO";
my $datetime=<file>;
my $in_out=<file>;
close FILE;

if ($in_out =~ m/out/)
{#no hay nadie
    syslog ("user","Nadie en casa,... voy mirando");
    unless (-e $ROOMBAPID) 
    {#Si no estaba la roomba funcionando ya, vemos a ver si hay que sacarla o no
        ($sec, $min, $hour, $mday, $mon, $year, $weekday) = localtime(time()); #fecha/hora actual
        my $date= sprintf('%02d-%02d-%d', $mday, $mon + 1, $year + 1900);
        my $time= sprintf('%02d:%02d:%02d', $hour, $min, $sec);
        if (($weekday >=1) && ($weekday <=5)) 
        {#Si estamos en día laborable
            $horamax="20:30:00" ;#Hora maxima de salida de Roomba
            $horamin="09:30:00"; #Hora minima de salida de Roomba
        }
        else
        {#Estamos en fin de semana
            $horamax="21:00:00" ;#Hora maxima de salida de Roomba
            $horamin="11:15:00"; #Hora minima de salida de Roomba
        }

        my $dtnow = DateTime->now(time_zone=>'Europe/Madrid');
        my @maxtime=split (/:/, $horamax);
        my $dtMax = DateTime->new(
                       year => $year+1900,
                       month  => $mon+1,
                       day    => $mday,
                       hour => @maxtime[0],
                       minute => @maxtime[1],
                       second => @maxtime[2]
                       );
        my @mintime=split (/:/, $horamin);
        my $dtMin = DateTime->new(
                       year => $year+1900,
                       month  => $mon+1,
                       day    => $mday,
                       hour => @mintime[0],
                       minute => @mintime[1],
                       second => @mintime[2]
                       );
        
        if (($dtnow > $dtMin) && ($dtnow <$dtMax))
        {#Estamos en horario de limpieza, seguimos
            syslog ("user","Estamos en horario de limpieza");
            if (mas_de_x($datetime,$segundos))
            {#Si ha pasado el tiempo de no-deteccion, seguimos mirando
                syslog ("user","Ha pasado el tiempo de margen...");                 
                #Miro si la roomba ha salido hoy 
                open HISTORY, "<$history";
                my $last=<history>;
                close HISTORY;
                chomp ($last);
                if ($last ne $date)
                {#Si no he salido hoy
                    @roombastatus=get_roomba_status;
                    my $cargamax= ($roombastatus[24] * 255) + $roombastatus[25];
                    my $cargaactual = ($roombastatus[22] * 255) + $roombastatus[23];
                    syslog ("user", "cargamax $cargamax cargaactual $cargaactual");
                    if ($roombastatus[39] eq 2)
                    {#Estaba cargandose
                         my $ratio=$cargaactual/$cargamax;
                         syslog ("user","RATIO $ratio");
                         if ($ratio>$rangobattpermitido)#Si la batería está más cargada del $rangobattpermitido (90% en mi caso)                       
                         {#Entonces saco a la roomba //Controlar tiempo hecho por la roomba y permitir que vuelva a salir si ha hecho poco tiempo
                            syslog ("user","Como aun no he salido hoy,... arranca Roomba!");
                            open HISTORY, ">$history";
                            print HISTORY $date;#Guardo la fecha de hoy en $history  
                            close (HISTORY);
                            system "/usr/local/bin/roomba.pl clean 0";#Y le damos la orden de limpiar
                            system "touch $ROOMBAPID"; #indicamos un fichero de lock para saber que esta la roomba limpiando
                            system ("/usr/local/bin/notifier.pl 1 \"Acabo de encender la Roomba\"");#Me aviso que la roomba sale a funcionar
                        }
                     }
                     else {syslog ("user","Me estoy cargando!");}#La carga era menor de lo permitido para salir (90%) 
                }
                else
                {#Estaba dando vueltas por ahi, monitorizaremos la temperatura
                     if ($roombastatus[21] > $maxtemp)
                     {#Avisar si la temperatura es mayor q la permitida
                        system ("/usr/local/bin/notifier.pl 1 \"Temp de la batt de la roomba es $roombastatus[21]\"");
                     }
                } 
            }#Si no, esperamos el margen de tiempo antes de empezar 
        }#No estamos en horario de limpieza
    }
    else
    {#Estaba ya funcionando: validamos si se esta o no cargando y sigue habiendo ROOMBAPID
        my @roombastatus = get_roomba_status;
        if ($roombastatus[39] ne 2)
        {#Si no esta cargandose miramos como esta la temp de la batt
                    if ($roombastatus[21] > $maxtemp)
                    {#Avisar si la temperatura es mayor q la permitida
                        system ("/usr/local/bin/notifier.pl 1 \"Temp de la batt de la roomba es $roombastatus[21]\"");
                    }
        }
        else
        {#Ha terminado sola su ciclo de limpieza y se ha ido a la base sola pero sigue sin haber nadie
            unlink $ROOMBAPID;
        }
    }
}
else
{#hay alguien en casa
    if (-e $ROOMBAPID)
    {#Si estaba la roomba dando vueltas
        unlink $ROOMBAPID; #borramos el fichero
        my @roombastatus =get_roomba_status; #Miramos a ver si ya estaba en su base
        if ($roombastatus[39] eq 0)
        {#Si estaba dando vueltas la mandamos a su base y notificamos
            system ("/usr/local/bin/roomba.pl dock 0");    
            system ("/usr/local/bin/notifier.pl 1 \"Mando la roomba a su dock\"");#Me aviso que va a su dock
        }
    }
    elsif (!mas_de_x($datetime,margendock))
    {#No esta el fichero como andando pero la roomba no esta cargandose
        my @roombastatus = get_roomba_status;
        if ($roombastatus[39] ne 2)
        {
            system ("/usr/local/bin/roomba.pl dock 0");    
            system ("/usr/local/bin/notifier.pl 1 \"Mando la roomba a su dock, segunda vez!\"");
        }    
        if ($roombastatus[21] > $maxtemp)
        {#Avisar si la temperatura es mayor q la permitida
            system ("/usr/local/bin/notifier.pl 1 \"Temp de la batt de la roomba es $roombastatus[6]\"");
        }
    }
}


if ($pid !=0)
{
        kill 9,$pid;
}

unlink $pidfil;


myscheduler.pl


El script myscheduler.pl, que también se ejecuta cada minuto, lee de una base de datos si tiene algún evento programado para ese momento, y en ese caso, lo ejecuta y lo borra

[bot@Carmen ~]# more /usr/local/bin/myscheduler.pl 
#!/usr/bin/perl

use DBI;

my $dsn = 'DBI:mysql:home:localhost';
my $db_user_name = 'atitelovoyadecir';
my $db_password = 'esperaquevoy';

sub ejecuta_comando
{
    my @fields=split (/ /,$_[0]);
    if ($fields[0] =~ /aa/i)
    {#Comando es "aa" -> aire acondicionado
        if ($fields[1] =~ /on/i)
        {#Encendiendo
            my $zona = $fields[2]; #Termostato de la casa a encender
            my $temp = $fields[3]; #Temperatura a la que hay que encenderlo
            system "/usr/local/bin/aa_on_zone_temp.pl $zona $temp"; #Llamo a programa externo para encendido
        }
        elsif ($fields[1] =~ /off/i)
        {#Apagando zona aire acondicionado
            my $zona= $fields[2];#Zona a apagar
            system "/usr/local/bin/set_zona_off.pl $zona"; #Llamada a programa que apagará un termostato
        }
    }
    elsif ($fields[0] =~ /recuerdame/i)
    {#Comando para recordarme cosas a determinadas horas
        my $str = substr($_[0],11,length ($_[0]));#quitamos la palabra "recuerdame " de lo q queremos
        system "/usr/local/bin/notifier.pl 3 \"$str\"";#Avisame de eso que tenía que acordarme...
    
    }
    #else... futuros comandos
}


#MAIN
my $dbh = DBI->connect($dsn, $db_user_name, $db_password);

my ($sec, $min, $hour, $mday, $mon, $year) = localtime(time());#Fecha/Hora
$mon++;
#usaremos $min,$hour,$mon,$mday

my $sth=$dbh->prepare("select * from sched where dia=\'$mday\' and mes= \'$mon\' and hora=\'$hour\' and minuto=\'$min\'");#Miro si hay algo para YA
$sth->execute();

while (my ($crap,$crap,$crap,$crap,$comando)=$sth->fetchrow_array())
{#Para cada evento programado, ejecutamos AHORA
    ejecuta_comando($comando);
}

my $sth=$dbh->prepare("delete from sched where dia=\'$mday\' and mes=\'$mon\' and hora=\'$hour\' and minuto=\'$min\'");#Borramos los eventos ejecutados
$sth->execute();


Conclusiones
  • Si has llegado vivo hasta este punto, primero: Felicidades y gracias por no haberte perdido entre todo ese código. Segundo: habrás visto que llamo a otros tres scripts que no he detallado en este artículo. Son notifier.pl y dos relativos a modificar un termostato de los cuatro que hay por casa (aa_on_zone_temp.pl y set_sona_off.pl). De ambos sistemas, y por no recargar (aún más) este post, los dejo para publicaciones posteriores.
  • Estoy seguro que todo este maremagnum de scripts se puede hacer en menos líneas, más limpio y más ordenado, sin embargo, y siendo que mi sistema domótico casero ha ido evolucionando con los años, he tenido que ir adaptándome a lo que había e integrar las nuevas funcionalidades en estos scripts.

10 comments :

Luis Novoa dijo...

¡Alucinante!

Madrikeka dijo...

Madre mia!! Es increíble la que tiene "apañada" en casa.  XD 

Kanelus dijo...

Cualquier día la casa no te deja entrar y se hace con el control del edificio, luego manzana, calle, ciudad, país, planeta.....UNIVERSO? XDDD

Muy buena Loren!!

silverhack dijo...

Impresionante titi....
Eres una puta máquina!! 

A.Maldad dijo...

Hola, yo quería saber si el modem que te dejé lo sigues teniendo aplicado a alguna tarea o ya no está haciendo nada, a ver qué vida, por qué no aparezco en los créditos?

Lorenzo_Martinez dijo...

 Aurora, tu modem no es un voice modem, así que no valía para hacer de contestador automático... Lo tengo guardado de backup por si acaso el actual casca, poder usarlo para FAX llegado el momento. Si lo quieres, cuando vuelva a ir a Logroño te lo devuelvo :D

A.Maldad dijo...

 No hace falta que lo traigas de momento, sabiendo que hace de back up, me dejas más tranquila.

aldreas dijo...

claramente has pensado que el primer paso para dominar el mundo es dominar tu propia casa y estás trabajando en ello ;-)

Dfpluc dijo...

Muy Futurista, felicitaciones, keep on going!!

Invitado dijo...

Qué chulo, yo llevo tiempo con ganas de montar algo parecido, pero me falta lo principal... casa propia!