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

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

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

Прошивка для 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: %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)


  1. DaemonGloom
    06.03.2017 12:06
    +2

    А в 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
    При этом уберёте кучу зависимостей от конкретного роутера и прошивки.


    1. vvzvlad
      06.03.2017 17:02
      +1

      Ха, и напоретесь на интересное поведение девайсов от Apple(может и от других тоже) — они с включённым wifi перестают отвечать на пинги через минут десять после засыпания/выключения. Более того, они вроде и аренду не продлевают. Единственное место, откуда их можно вытащить — список подключённых клиентов(wifi associated list, емнип, в микротиках называется). Все остальное работает ненадежно.


  1. proton17
    06.03.2017 12:25

    Очень много "если" в таком способе. Если говорить про "Умный дом", то достаточно 3-4 датчиков присутствия и не важно есть ли у кого-то смартфон и включен ли там Wi-Fi.


    1. LexB
      06.03.2017 12:36

      Согласен — датчики надежнее, фундаментальнее. Это умный дом без кавычек.
      С другой стороны датчики надо приобрести, разместить, настроить, запитать, обслуживать. А тут вообще без вложений можно покрыть текущие нужды, из разряда «дешево и сердито», «умный дом» в кавычках.


      1. DnD_designer
        06.03.2017 14:46

        Датчики движения могут выполнять еще одну полезную функцию — охранной сигнализации в ваше отсутсвие (особенно, на момент отпуска или командировки).


      1. nikkadim
        08.03.2017 10:09

        Сенсоры движения скажут вам дома ли собака с кошкой, но не хозяин.


  1. instalator
    06.03.2017 12:41
    +1

    У себя я просто использую пинг телефона. Можно использовать приложение для андроид телефона — таскер и при подключении к домашней сети выдавать команду серверу УД.


  1. memtew
    06.03.2017 12:46
    +1

    Опция dhcp-scipt у Dnsmasq позволяет при выдаче каждого IP выполнять произвольный скрипт, чем я и пользуюсь.


  1. telobezumnoe
    06.03.2017 13:48
    +1

    на есп8266 в скетче использую библиотеку ping раз в десять секунд проверяю нужный мне айпишник, который вручную забит на телефоне и находится вне диапазона выдаваемого dhcp. при отсутствии меня дома или жены, публикуется статус в mqtt и включается ip cam и уже по движению ведет запись. всего пару строк кода, и схожий функционал.


  1. AllexIn
    06.03.2017 13:58
    +3

    Все таки ИМХО пожарную-охранную сигнализацию надо ставить первым делом. За долго до контроллеров лампочек и аудиосистем. А дальше — уже просто. Дом на охране — значит вас в нем нет.


  1. geisha
    06.03.2017 14:00

    Вообще-то ESPшка может хватать пакеты из эфира на штатной прошивке. Там MAC plain-textом записан. Минусы: не будет работать на 5Ghz и если телефон отключает WiFi при выключенном экране.


    1. safari2012
      06.03.2017 15:12
      +1

      Я бы вообще не стал вешать ESP-шку на общий SSID с другими устройствами. Лучше её выделить в отдельный SSID, благо большинство современных роутеров поддерживают виртуальные wi-fi сети.
      Не знаю, как от одной, но от нескольких ESP-шек скорость wi-fi сильно проседает. Это известная проблема реализации стека на данном MCU.


      1. vvzvlad
        06.03.2017 17:04

        А что, проседание скорости на виртуальной сети, которая висит на том же физическом радио, никак не затронет основную сеть?


        1. safari2012
          07.03.2017 02:39

          На виртуальной нормально. Проверено лично.


      1. geisha
        06.03.2017 18:12

        Я про promiscuous mode. Ваш комментарий я не совсем понял, ибо, в моём представлении, ESPшка молчит и ничего сама в эфир не шлёт.


        1. safari2012
          07.03.2017 02:38

          Если так, то да. Но весь эфир может не потянуть.


  1. Artemiy117
    06.03.2017 16:40
    -1

    А как быть если телефон сел или потерял сеть?
    Ночью, например телефон сел, и еспишка включила охрану. Ночью пошел в туалет, а тут тебе сирена врубилась. :)


    1. AlexGluck
      06.03.2017 17:54

      Значит заодно и по большому сходите.


  1. danyaShep
    06.03.2017 21:54
    -1

    И почему все первым делом изобретают велосипед?
    https://home-assistant.io/components/#presence-detection


  1. IRT
    07.03.2017 07:43

    Надежнее датчик движения в прихожей + геркон на входной двери. Закрылась дверь и нет движения в прихожей = дома никого нет.


    1. LexB
      07.03.2017 10:59

      Тоже об этом думал. Сработает если вы холостяк, иначе датчики надо ставить во всех комнатах, и то, если супруга не шевелится (спит) алгоритм не сработает.


  1. itscript
    07.03.2017 07:44
    +1

    А ничего, что у iphone MAC адрес меняется динамически?


    1. Xaser
      07.03.2017 10:59
      +1

      ничего