Определение того, что вы дома, используя WiFi-роутер (для автоматизации «умного дома»)

в 8:42, , рубрики: arduino, esp8266

В предыдущей статье я описал устройство для управления климатом на ESP8266. Возникает вопрос, а при каких событиях мы должны выполнять это управление? Самое простое — при наступлении определенного времени.

Второе что приходит в голову — присутствие в доме. Если вас нет дома, то нет смысла (или есть?) проветривать, отапливать и кондиционировать помещение.

В этой статье рассмотрим возможность определения присутствия используя wifi роутер. Нет, мы не будем следить за людьми сквозь стены используя wifi сигнал, а воспользуемся страничкой состояния в веб интерфейсе wifi роутера, и по наличию в списке вашего смартфона сможем понять дома вы или нет.

Естественно этот способ не сработает, если вы, или кто-то из вашей семьи, не использует смартфон, отключает wifi, если у вас нет wifi, уходя, оставляет устройство дома. Также в статье описан «рецепт» для конкретного dlink роутера. Если у вас другая модель — вероятно, что вам придется «доработать напильником».

Страница, отображающая список wifi клиентов выглядит примерно так:

Определение того, что вы дома, используя WiFi-роутер (для автоматизации «умного дома») - 1

Для доступа к этой странице нам необходимо авторизоваться на роутере. Изучаем исходный код страницы ввода пароля и видим:

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 своего смартфона в этом списке, мы легко определим дома вы* или нет.

Прошивка для ESP8266 получилась такая

* Из-за ограничений на объем доступной памяти нельзя просто загрузить страницы в память целиком и выполнять поиск/парсинг в памяти. Пришлось использовать примитивный сканер для поиска по шаблону. Заранее прошу прощение за код у знатоков и благодарю за конструктивные предложения по модификации.
#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: %dn", 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: %sn", 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(".");
  }
}

Прошивка периодически выводит в консоль список подключенных устройств.

Автор: LexB

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js