Summer holidays are over. Besides my bush walks I’ve been also hacking a little bit with one idea that I had in mind. Summer means high temperatures and I wanted to control my fan. For example turn on the fan when temperature is over a threshold. I can do it using an Arduino board and a temperature sensor, but I don’t have the one Arduino board. I have several devices. For example a Wemo switch. With this device connected to my Wifi network I can switch on and off my fan remotely from my mobile phone (using its android app) or even from my Pebble watch using the API. I also have a BeeWi temperature/humidity sensor. It’s a BTLE device. It comes with its own app for android, but there’s also a API. Yes. I known that one Arduino board with a couple of sensors can be cheaper than one of this devices, but when I’m a shop and I’ve got one of this devices in my hands I cannot resist.
I also have a new Raspberry pi 3. I’ve recently upgraded my home multimedia server from a rpi2 to the new rpi3. Basically I use it as multimedia server and now also as retro console. This new rpi3 has Bluetooth so I wanted to do something with it. Read temperature from the Bluetooth sensor sounds good so I started to hack a little bit.
I found this post. I started working with Python. The script almost works but it uses Bluetooth connection and as someone said in the comments it uses a lot of battery. So I switched to a BTLE version. I found a simple node library to connect BTLE devices called noble, really simple to use. In one afternoon I had one small script ready. The idea was put this script in my RP3’s crontab, and scan the temperature each minute (via noble) and if the temperature was over a threshold switch on the wemo device (via ouimeaux). I also wanted to be informed when my fan is switch on and off. The most easier way to do it was via Telegram (I already knew telebot library).
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);
The script is here.
It works but I wanted to keep on hacking. One Sunday morning I read this post. I don’t have an amazon button, but I wanted to do something similar. I started to play with scapy library sniffing ARP packets in my home network. I realize that I can detect when my Kindle connects to the network, my tv, or even my mobile phone. Then I had one I idea: Detect when my mobile phone connects to my wifi. My mobile phone connects to my wifi before I enter in my house so my idea was simple: Detect when I’m close to my home’s door and send me a telegram message saying “Wellcome home” in addition to the temperature inside my house at this moment.
#!/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)
I have one node script to read temperature and one Python script to sniff my network. I can find how to read temperature from Python and use only one script but I was lazy (remember that I was on holiday) so I turned the node script that reads temperature into a 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);
Now I only need to call this worker from my Python sniffer and thats all.
I wanted to play a little bit. I also wanted to ask the temperature on demand. Since I was using Telegram I had an idea. Create a Telegram bot running in my RP3. And that’s my summer pet project. Basically it has three parts:
worker.js It’s a gearman worker. It reads temperature and humidity from my BeeWi sensor via BTLE
bot.py It’s a Telegram bot with the following commands available:
/switchInfo: get switch info /switchOFF: switch OFF the switch /help: Gives you information about the available commands /temp: Get temperature /switchON: switch ON the switch
sniff.py It’s just a ARP sniffer. It detects when I’m close to my home and sends me a message via Telegram with the temperature. It detects when my mobile phone sends a ARP package to my router (aka when I connect to my Wifi). It happens before I enter in my house, so the Telegram message arrives before I put the key in the door :)
I run al my scripts in my Raspberry Pi3. To ensure all scripts are up an running I use supervisor
All the scripts are available in my github account