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í:
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 :
¡Alucinante!
Madre mia!! Es increíble la que tiene "apañada" en casa. XD
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!!
Impresionante titi....
Eres una puta máquina!!
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?
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
No hace falta que lo traigas de momento, sabiendo que hace de back up, me dejas más tranquila.
claramente has pensado que el primer paso para dominar el mundo es dominar tu propia casa y estás trabajando en ello ;-)
Muy Futurista, felicitaciones, keep on going!!
Qué chulo, yo llevo tiempo con ganas de montar algo parecido, pero me falta lo principal... casa propia!
Publicar un comentario