Back-End

30 set, 2016

Projeto de estimação de automação de casa – Brincando com IoT, sensores de temperatura, ventiladores e bots do Telegram

Publicidade

As férias de verão acabaram. Além das minhas caminhadas, eu codifiquei um pouco uma ideia que eu tinha na cabeça. Verão significa altas temperaturas, e eu queria controlar meu ventilador. Por exemplo, ligar o ventilador quando a temperatura é superior a um limite. Eu posso fazer isso usando uma placa Arduino e um sensor de temperatura, mas eu não tenho uma placa Arduino. Eu tenho vários dispositivos. Por exemplo, um interruptor Wemo. Com esse dispositivo conectado à minha rede Wi-Fi, eu posso ligar e desligar meu ventilador remotamente a partir do celular (usando o aplicativo Android) ou do meu relógio Pebble usando a API. Eu também tenho um sensor de temperatura/umidade BeeWi. É um dispositivo BTLE. Ele vem com seu próprio aplicativo para Android, mas há também uma API. Isso. Eu sei que uma placa Arduino com alguns sensores pode ser mais barato do que um desses dispositivos, mas quando eu fui às compras e coloquei as mãos em um desses dispositivos, eu não pude resistir.

Eu também tenho um novo Raspberry Pi 3. Eu recentemente atualizei meu servidor multimídia de casa do rpi2 para o novo rpi3. Basicamente, eu o uso como servidor multimídia e agora também como console retrô. Esse novo rpi3 tem Bluetooth, então eu queria fazer algo com ele. Ler a temperatura a partir do sensor Bluetooth soa bem, então eu comecei a hackear um pouco.

Eu encontrei este artigo. Comecei a trabalhar com Python. O script quase funciona, mas ele usa a conexão Bluetooth e, como alguém disse nos comentários, ele usa muita bateria. Então eu mudei para uma versão BTLE. Eu encontrei uma biblioteca node simples para conectar dispositivos BTLE chamados noble, muito simples de usar. Em uma tarde, eu tinha um pequeno script pronto. A ideia foi colocar esse script no crontab do meu RP3, e monitorar a temperatura a cada minuto (via noble) e se a temperatura fosse mais alta do que um limite, ligar o interruptor no dispositivo Wemo (via ouimeaux). Eu também queria ser informado quando o meu ventilador fosse ligado e desligado. A maneira mais fácil de fazer isso era através do Telegram (eu já sabia como usar a biblioteca telebot).

var noble = require('noble'),
    Wemo = require('wemo-client'),
    TeleBot = require('telebot'),
    fs = require('fs'),
    beeWiData,
    wemo,
    threshold,
    address,
    bot,
    chatId,
    wemoDevice,
    configuration,
    confPath;
 
if (process.argv.length <= 2) {
    console.log("Usage: " + __filename + " conf.json");
    process.exit(-1);
}
 
confPath = process.argv[2];
try {
    configuration = JSON.parse(
        fs.readFileSync(process.argv[2])
    );
} catch (e) {
    console.log("configuration file not valid");
    process.exit(-1);
}
 
bot = new TeleBot(configuration.telegramBotAPIKey);
address = configuration.beeWiAddress;
threshold = configuration.threshold;
wemoDevice = configuration.wemoDevice;
chatId = configuration.telegramChatId;
 
function persists() {
    configuration.beeWiData = beeWiData;
    fs.writeFileSync(confPath, JSON.stringify(configuration));
}
 
function setSwitchState(state, callback) {
    wemo = new Wemo();
    wemo.discover(function(deviceInfo) {
        if (deviceInfo.friendlyName == wemoDevice) {
            console.log("device found:", deviceInfo.friendlyName, "setting the state to", state);
            var client = wemo.client(deviceInfo);
            client.on('binaryState', function(value) {
                callback();
            });
 
            client.on('statusChange', function(a) {
                console.log("statusChange", a);
            });
            client.setBinaryState(state);
        }
    });
}
 
beeWiData = {temperature: undefined, humidity: undefined, batery: undefined};
 
function hexToInt(hex) {
    if (hex.length % 2 !== 0) {
        hex = "0" + hex;
    }
    var num = parseInt(hex, 16);
    var maxVal = Math.pow(2, hex.length / 2 * 8);
    if (num > maxVal / 2 - 1) {
        num = num - maxVal;
    }
    return num;
}
 
noble.on('stateChange', function(state) {
    if (state === 'poweredOn') {
        noble.stopScanning();
        noble.startScanning();
    } else {
        noble.stopScanning();
    }
});
 
noble.on('scanStop', function() {
    var message, state;
    if (beeWiData.temperature > threshold) {
        state = 1;
        message = "temperature (" + beeWiData.temperature + ") over threshold (" + threshold + "). Fan ON. Humidity: " + beeWiData.humidity;
    } else {
        message = "temperature (" + beeWiData.temperature + ") under threshold (" + threshold + "). Fan OFF. Humidity: " + beeWiData.humidity;
        state = 0;
    }
    setSwitchState(state, function() {
        if (configuration.beeWiData.hasOwnProperty('temperature') && configuration.beeWiData.temperature < threshold && state === 1 || configuration.beeWiData.temperature > threshold && state === 0) {
            console.log("Notify to telegram bot", message);
            bot.sendMessage(chatId, message).then(function() {
                process.exit(0);
            }, function(e) {
                console.error(e);
                process.exit(0);
            });
            persists();
        } else {
            console.log(message);
            persists();
            process.exit(0);
        }
    });
});
 
noble.on('discover', function(peripheral) {
    if (peripheral.address == address) {
        var data = peripheral.advertisement.manufacturerData.toString('hex');
        beeWiData.temperature = parseFloat(hexToInt(data.substr(10, 2)+data.substr(8, 2))/10).toFixed(1);
        beeWiData.humidity = Math.min(100,parseInt(data.substr(14, 2),16));
        beeWiData.batery = parseInt(data.substr(24, 2),16);
        beeWiData.date = new Date();
        noble.stopScanning();
    }
});
 
setTimeout(function() {
    console.error("timeout exceded!");
    process.exit(0);
}, 5000);

O script está aqui.

Funciona, mas eu queria continuar hackeando. Em um domingo de manhã, eu li este texto. Eu não tenho um botão da Amazon, mas eu queria fazer algo similar. Comecei a brincar com a biblioteca de pacotes ARP scapy sniffing na minha rede doméstica. Percebi que posso detectar quando o meu Kindle se conecta à rede, à minha TV, ou até mesmo ao meu telefone celular. Então eu tive uma ideia: detectar quando o meu telefone celular se conecta ao meu Wi-Fi. Meu celular se conecta ao meu Wi-Fi antes de eu entrar na minha casa, então a minha ideia era simples: detectar quando estou perto da porta de minha casa e me enviar uma mensagem no Telegram dizendo “Bem-vindo” em conjunto com a temperatura dentro da minha casa no momento.

#!/usr/bin/env python
 
import sys
from scapy.all import *
import telebot
import gearman
import json
from StringIO import StringIO
 
BUFFER_SIZE = 1024
 
try:
    with open(sys.argv[1]) as data_file:
        data = json.load(data_file)
        myPhone = data['myPhone']
        routerIP = data['routerIP']
        TOKEN = data['telegramBotAPIKey']
        chatID = data['telegramChatId']
        gearmanServer = data['gearmanServer']
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise
 
def getSensorData():
    gm_client = gearman.GearmanClient([gearmanServer])
    completed_job_request = gm_client.submit_job("temp", '')
    io = StringIO(completed_job_request.result)
 
    return json.load(io)
 
tb = telebot.TeleBot(TOKEN)
 
def arp_display(pkt):
    if pkt[ARP].op == 1 and pkt[ARP].hwsrc == myPhone and pkt[ARP].pdst == routerIP:
        sensorData = getSensorData()
        message = "Wellcome home Gonzalo! Temperature: %s humidity: %s" % (sensorData['temperature'], sensorData['humidity'])
        tb.send_message(chatID, message)
        print message
 
print sniff(prn=arp_display, filter='arp', store=0)

Eu tenho um script node para ler a temperatura e um script Python para rastrear minha rede. Eu posso encontrar uma forma de ler a temperatura em Python e usar apenas um script, mas eu estava com preguiça (lembre que eu estava de férias), então eu transformei o script node que lê a temperatura em um gearman worker.

var noble = require('noble'),
    fs = require('fs'),
    Gearman = require('node-gearman'),
    beeWiData,
    address,
    bot,
    configuration,
    confPath,
    status,
    callback;
 
var gearman = new Gearman();
 
if (process.argv.length <= 2) {
    console.log("Usage: " + __filename + " conf.json");
    process.exit(-1);
}
 
confPath = process.argv[2];
try {
    configuration = JSON.parse(
        fs.readFileSync(process.argv[2])
    );
} catch (e) {
    console.log("configuration file not valid", e);
    process.exit(-1);
}
 
address = configuration.beeWiAddress;
delay = configuration.tempServerDelayMinutes * 60 * 1000;
tcpPort = configuration.tempServerPort;
 
beeWiData = {};
 
function hexToInt(hex) {
    if (hex.length % 2 !== 0) {
        hex = "0" + hex;
    }
    var num = parseInt(hex, 16);
    var maxVal = Math.pow(2, hex.length / 2 * 8);
    if (num > maxVal / 2 - 1) {
        num = num - maxVal;
    }
    return num;
}
 
noble.on('stateChange', function(state) {
    if (state === 'poweredOn') {
        console.log("stateChange:poweredOn");
        status = true;
    } else {
        status = false;
    }
});
 
noble.on('discover', function(peripheral) {
    if (peripheral.address == address) {
        var data = peripheral.advertisement.manufacturerData.toString('hex');
        beeWiData.temperature = parseFloat(hexToInt(data.substr(10, 2)+data.substr(8, 2))/10).toFixed(1);
        beeWiData.humidity = Math.min(100,parseInt(data.substr(14, 2),16));
        beeWiData.batery = parseInt(data.substr(24, 2),16);
        beeWiData.date = new Date();
        noble.stopScanning();
    }
});
 
noble.on('scanStop', function() {
    console.log(beeWiData);
    noble.stopScanning();
    callback();
});
 
var worker;
 
function workerCallback(payload, worker) {
    callback = function() {
        worker.end(JSON.stringify(beeWiData));
    }
 
    beeWiData = {temperature: undefined, humidity: undefined, batery: undefined};
 
    if (status) {
        noble.stopScanning();
        noble.startScanning();
    } else {
        setInterval(function() {
            workerCallback(payload, worker);
        }, 1000);
    }
}
 
gearman.registerWorker("temp", workerCallback);

Agora eu só preciso chamar esse worker do meu rastreador Python, e pronto.

Eu queria brincar um pouco. Também queria perguntar a temperatura em demanda. Já que eu estava usando o Telegram, eu tive uma ideia: criar um bot Telegram rodando no meu RP3. E esse é o meu projeto de estimação de verão. Basicamente, ele tem três partes:

worker.js

É um gearman worker. Ele lê a temperatura e a umidade do meu sensor BeeWi via BTLE.

bot.py

É um bot Telegram com os seguintes comandos disponíveis:

/SwitchInfo: pega a informação do interruptor

/SwitchOff: desliga o interruptor

/Help: informa sobre os comandos disponíveis

/Temp: pega a temperatura

/SwitchON: liga o interruptor

sniff.py

É apenas um sniffer ARP. Ele detecta quando estou perto de minha casa e me envia uma mensagem via Telegram com a temperatura. Ele detecta quando meu telefone celular envia um pacote ARP para o meu roteador (aka quando eu me conectar ao meu Wi-Fi). Isso acontece antes de eu entrar na minha casa, para que a mensagem do Telegram chegue antes de eu colocar a chave na porta ?.

Eu rodo os meus scripts no meu Raspberry Pi3. Para garantir que todos os scripts estão rodando, eu uso supervisor.

Todos os scripts estão disponíveis na minha conta GitHub.

***

Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://gonzalo123.com/2016/08/29/home-automation-pet-project-playing-with-iot-temperature-sensors-fans-and-telegram-bots/.