Второе что приходит в голову — присутствие в доме. Если вас нет дома, то нет смысла (или есть?) проветривать, отапливать и кондиционировать помещение.
В этой статье рассмотрим возможность определения присутствия используя wifi роутер. Нет, мы не будем следить за людьми сквозь стены используя wifi сигнал, а воспользуемся страничкой состояния в веб интерфейсе wifi роутера, и по наличию в списке вашего смартфона сможем понять дома вы или нет.
Естественно этот способ не сработает, если вы, или кто-то из вашей семьи, не использует смартфон, отключает wifi, если у вас нет wifi, уходя, оставляет устройство дома. Также в статье описан «рецепт» для конкретного dlink роутера. Если у вас другая модель — вероятно, что вам придется «доработать напильником».
Страница, отображающая список wifi клиентов выглядит примерно так:
Для доступа к этой странице нам необходимо авторизоваться на роутере. Изучаем исходный код страницы ввода пароля и видим:
1) в странице ввода пароля роутер отправляет salt и authid.
2) роутер берет из пароля первые 16 цифр, объединяет их с salt, «добивает» строку символом chr(1) до 64х символов.
3) Для полученной 64x символьной строки считает MD5.
4) объединяет salt + md5
5) формирует строку вида
http://192.168.0.1/post_login.xml?hash=a33403f9aded48e57FF9e09d37d9009026e1ce85&auth_code=&auth_id=09CFF
где hash это строка полученная в п4., auth_id — строка полученная в п1.
6) если авторизация прошла успешно, то роутер возвращает xml с адресом страницы для редиректа.
Код примерно следующий:
var salt = 'a33403f9';
var password = document.forms.myform.old_password.value;
password = password.substr(0,16);
for (var i = password.length; i < 16; i++) {
password += String.fromCharCode(1);
}
var input = salt + password;
for (var i = input.length; i < 63; i++) {
input += String.fromCharCode(1);
}
input += (document.forms.myform.old_username.value == 'user') ? 'U' : String.fromCharCode(1);
var hash = hex_md5(input);
var login_hash = salt.concat(hash);
var auth_url = '';
auth_url = '&auth_code=' + document.forms.myform.auth_code.value + '&auth_id=09C05';
var xml_loader = new ajax_xmlhttp('/post_login.xml?hash=' + login_hash + auth_url, xml_ready, xml_timeout);
После того как мы авторизовались на страничке роутера достаточно запросить:
http://192.168.0.1/wifi_assoc.xml
и мы получим XML вида:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<wifi_assoc>
<radio>
<assoc>
<mac>18FEFFFFCCFF</mac>
<ssid>bingo</ssid>
<channel>8</channel>
<rate>65</rate>
<quality>100</quality>
<type>802.11n (2.4GHz)</type>
<ip_address>192.168.0.3</ip_address>
</assoc>
<assoc>
<mac>001DFEFF70FF</mac>
<ssid>bingo</ssid>
<channel>8</channel>
<rate>65</rate>
<quality>84</quality>
<type>802.11n (2.4GHz)</type>
<ip_address>192.168.0.4</ip_address>
</assoc>
<assoc>
<mac>AC37FFFFDCFF</mac>
<ssid>bingo</ssid>
<channel>8</channel>
<rate>104</rate>
<quality>100</quality>
<type>802.11n (2.4GHz)</type>
<ip_address>192.168.0.5</ip_address>
</assoc>
<assoc>
<mac>18FEFFFFFFDF</mac>
<ssid>bingo</ssid>
<channel>8</channel>
<rate>58</rate>
<quality>100</quality>
<type>802.11n (2.4GHz)</type>
<ip_address>192.168.0.6</ip_address>
</assoc>
</radio>
</wifi_assoc>
Проверив наличие MAC своего смартфона в этом списке, мы легко определим дома вы* или нет.
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <MD5Builder.h>
MD5Builder _md5;
HTTPClient http;
const char* ssid = "bingo";
const char* password = "qqq_zzz_xxx";
const char* ap_ssid = "esp";
const char* ap_password = "espqw3454#1";
// salt
const char* patternSalt = "var salt = \"";
const int patternsaltLen = strlen(patternSalt);
int patternsaltPos = 0;
char salt[20] = "";
int saltLen = sizeof(salt);
int saltPos = 0;
// auth
const char* patternAuthID = "\"&auth_id=";
const int patternAuthIDLen = strlen(patternAuthID);
int patternAuthIDPos = 0;
char authID[20] = "";
int authIDLen = sizeof(authID);
int authIDPos = 0;
// mac
const char* patternMac = "<mac>";
const int patternMacLen = strlen(patternMac);
int patternMacPos = 0;
char mac[20] = "";
int macLen = sizeof(mac);
int macPos = 0;
typedef void (*patternSearchFinishedHandler)();
void patternSearchFinishedDummy() {}
void patternSearchFinishedMac() {
Serial.print("mac=");
Serial.println(mac);
mac[0] = (char) 0;
macPos = 0;
}
void setup() {
Serial.begin(115200);
WiFi.softAP(ap_ssid, ap_password);
WiFi.mode(WIFI_AP_STA);
WiFi.begin(ssid, password);
Serial.println("Setup Completed");
}
void charBuf_to_u8buf(const char buf1[128], uint8_t buf2[128], int buffSize){
for (int i=0; i < buffSize; i++){
buf2[i] = (uint8_t)buf1[i];
}
}
void u8buf_to_charBuf(const uint8_t buf1[128], char buf2[128], int buffSize){
for (int i=0; i < buffSize; i++){
buf2[i] = (char)buf1[i];
}
}
void checkPattern(int* tpos, int tlen, char c, const char* templ, char* data, int* datapos, int datalen, char finishChar, patternSearchFinishedHandler handler){
if (*tpos == tlen)
{
if (finishChar == c){
if (patternSearchFinishedDummy != handler){
delay(10);
handler();
}
*tpos = 0;
}
else
{
if (*datapos < datalen-2){
data[*datapos] = c;
data[*datapos + 1] = (char) 0;
*datapos += 1;
}
}
}
else
{
if (templ[*tpos] == c){
*tpos += 1;
}else{
*tpos = 0;
}
}
}
void processBuffer(uint8_t buff[128], int buffSize){
char cbuf[128] = {};
u8buf_to_charBuf(buff, cbuf, buffSize);
for (int i=0; i < buffSize; i++){
checkPattern(&patternsaltPos, patternsaltLen, cbuf[i], patternSalt, salt, &saltPos, saltLen, '"', patternSearchFinishedDummy);
checkPattern(&patternAuthIDPos, patternAuthIDLen, cbuf[i], patternAuthID, authID, &authIDPos, authIDLen, '"', patternSearchFinishedDummy);
checkPattern(&patternMacPos, patternMacLen, cbuf[i], patternMac, mac, &macPos, macLen, '<', patternSearchFinishedMac);
}
}
String md5(String str) {
_md5.begin();
_md5.add(String(str));
_md5.calculate();
return _md5.toString();
}
void intVars() {
// init vars
salt[0] = (char) 0;
saltPos = 0;
authID[0] = (char) 0;
authIDPos = 0;
mac[0] = (char) 0;
macPos = 0;
}
void queryAddress(String address, bool dumpOutput, bool doProcessBuffer){
delay(10);
// configure server and url
http.begin(address);
// Serial.print("[HTTP] GET...\n");
// start connection and send HTTP header
int httpCode = http.GET();
if(httpCode > 0) {
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTP] GET... code: %d\n", httpCode);
// file found at server
if(httpCode == HTTP_CODE_OK) {
// get lenght of document (is -1 when Server sends no Content-Length header)
int len = http.getSize();
// create buffer for read
uint8_t buff[128] = { 0 };
// get tcp stream
WiFiClient * stream = http.getStreamPtr();
// read all data from server
while(http.connected() && (len > 0 || len == -1)) {
// get available data size
size_t size = stream->available();
if(size) {
// read up to 128 byte
int c = stream->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size));
if (doProcessBuffer){
processBuffer(buff,c);
}
// write it to Serial
if (dumpOutput){
Serial.write(buff, c);
}
if(len > 0) {
len -= c;
}
}
delay(10);
}
Serial.println();
Serial.print("[HTTP] connection closed or file end.\n");
}
} else {
Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
void queryData(){
intVars();
queryAddress("http://192.168.0.1/", false, true);
Serial.print("salt=");
Serial.println(salt);
Serial.print("authID=");
Serial.println(authID);
String data = "";
data.concat(salt);
// password
data.concat("bingo_fff#xxx ");
data = md5(data);
String addr = "http://192.168.0.1/post_login.xml?hash=";
addr.concat(salt);
addr.concat(data);
addr.concat("&auth_code=&auth_id=");
addr.concat(authID);
queryAddress(addr, false, false);
queryAddress("http://192.168.0.1/wifi_assoc.xml", false, true);
delay(10000);
}
void loop() {
// Wait for connection
if (WiFi.status() == WL_CONNECTED) {
queryData();
} else {
delay(500);
Serial.print(".");
}
}
Прошивка периодически выводит в консоль список подключенных устройств.
Комментарии (23)
proton17
06.03.2017 12:25Очень много "если" в таком способе. Если говорить про "Умный дом", то достаточно 3-4 датчиков присутствия и не важно есть ли у кого-то смартфон и включен ли там Wi-Fi.
LexB
06.03.2017 12:36Согласен — датчики надежнее, фундаментальнее. Это умный дом без кавычек.
С другой стороны датчики надо приобрести, разместить, настроить, запитать, обслуживать. А тут вообще без вложений можно покрыть текущие нужды, из разряда «дешево и сердито», «умный дом» в кавычках.DnD_designer
06.03.2017 14:46Датчики движения могут выполнять еще одну полезную функцию — охранной сигнализации в ваше отсутсвие (особенно, на момент отпуска или командировки).
instalator
06.03.2017 12:41+1У себя я просто использую пинг телефона. Можно использовать приложение для андроид телефона — таскер и при подключении к домашней сети выдавать команду серверу УД.
memtew
06.03.2017 12:46+1Опция dhcp-scipt у Dnsmasq позволяет при выдаче каждого IP выполнять произвольный скрипт, чем я и пользуюсь.
telobezumnoe
06.03.2017 13:48+1на есп8266 в скетче использую библиотеку ping раз в десять секунд проверяю нужный мне айпишник, который вручную забит на телефоне и находится вне диапазона выдаваемого dhcp. при отсутствии меня дома или жены, публикуется статус в mqtt и включается ip cam и уже по движению ведет запись. всего пару строк кода, и схожий функционал.
AllexIn
06.03.2017 13:58+3Все таки ИМХО пожарную-охранную сигнализацию надо ставить первым делом. За долго до контроллеров лампочек и аудиосистем. А дальше — уже просто. Дом на охране — значит вас в нем нет.
geisha
06.03.2017 14:00Вообще-то ESPшка может хватать пакеты из эфира на штатной прошивке. Там MAC plain-textом записан. Минусы: не будет работать на 5Ghz и если телефон отключает WiFi при выключенном экране.
safari2012
06.03.2017 15:12+1Я бы вообще не стал вешать ESP-шку на общий SSID с другими устройствами. Лучше её выделить в отдельный SSID, благо большинство современных роутеров поддерживают виртуальные wi-fi сети.
Не знаю, как от одной, но от нескольких ESP-шек скорость wi-fi сильно проседает. Это известная проблема реализации стека на данном MCU.vvzvlad
06.03.2017 17:04А что, проседание скорости на виртуальной сети, которая висит на том же физическом радио, никак не затронет основную сеть?
geisha
06.03.2017 18:12Я про promiscuous mode. Ваш комментарий я не совсем понял, ибо, в моём представлении, ESPшка молчит и ничего сама в эфир не шлёт.
Artemiy117
06.03.2017 16:40-1А как быть если телефон сел или потерял сеть?
Ночью, например телефон сел, и еспишка включила охрану. Ночью пошел в туалет, а тут тебе сирена врубилась. :)
danyaShep
06.03.2017 21:54-1И почему все первым делом изобретают велосипед?
https://home-assistant.io/components/#presence-detection
IRT
07.03.2017 07:43Надежнее датчик движения в прихожей + геркон на входной двери. Закрылась дверь и нет движения в прихожей = дома никого нет.
LexB
07.03.2017 10:59Тоже об этом думал. Сработает если вы холостяк, иначе датчики надо ставить во всех комнатах, и то, если супруга не шевелится (спит) алгоритм не сработает.
DaemonGloom
А в ESP8266 не работают стандартные методы определения IP устройств по mac адресу?
Обычно просто отправляют ICMP ECHO по адресу 255.255.255.255 и слушают ответы.
http://electronics.stackexchange.com/questions/170248/how-to-get-ip-address-from-mac-address-using-esp8266-wifi-module
При этом уберёте кучу зависимостей от конкретного роутера и прошивки.
vvzvlad
Ха, и напоретесь на интересное поведение девайсов от Apple(может и от других тоже) — они с включённым wifi перестают отвечать на пинги через минут десять после засыпания/выключения. Более того, они вроде и аренду не продлевают. Единственное место, откуда их можно вытащить — список подключённых клиентов(wifi associated list, емнип, в микротиках называется). Все остальное работает ненадежно.