Домашняя метеостанция на esp8266 + aqara-xiaomi, часть 2

Привет всем.

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

Домашняя метеостанция на esp8266 + aqara-xiaomi, часть 2

КДПВ.

Но это всё лирика, пора к делу!

Итак, что было сделано в новой версии системы.

Железячная часть (на esp8266):

  • Полностью переписан код системы: теперь повсеместно используется ООП для работы с сенсорами и дисплеями, для того, чтобы максимально упростить добавление новых датчиков, и сделать код понятным и структурированным
  • Учтены пожелания касательно безопасности системы
  • Упрощены страницы настройки модуля
  • Удалены зависимости на RTC

Серверная часть (php + mysql):

  • Изменен алгоритм рисования графиков — используется фильтр Калмана
  • Каждый пользователь системы теперь видит исключительно свои модули
  • Можно давать свои названия сенсорам, управлять их видимостью на страницах
  • Добавлена возможность просмотра данных с датчиков температуры-влажности производства Aqara-Xiaomi.

Хочу остановиться на наиболее интересных задачах, которые пришлось решить.

1. Программа в виде портянки из 1000+ строк кода стала совершенно нечитаемой, слишком сложно стало ориентироваться в этом коде, слишком сложно и затратно по времени стало проверять и отлаживать изменения. Поэтому код был переписан — появились базовые интерфейсы, объявляющие стандартные методы для работы с сенсорами и дисплеями, появились их наследники, реализующие логику, специфичную для конкретных моделей, а так же фабрики, ответственные собственно за создание нужных классов.

Покажу на примере:

#define sensorsCount 2 int sensorTypes[sensorsCount] = {SENSOR_DHT22, SENSOR_DHT22}; int sensorPins[sensorsCount] = {2, 0}; SensorEntity** sensorEntities; SensorOutputData* outputDatas; 

Здесь объявлено, что у нас есть два сенсора типа DHT22, подключенных к пинам 2 и 0, а так же объявлены массивы для классов-оберток над сенсорами, и для массива полученных от них данных.

DisplayEntity display = DisplayEntity(DISPLAY_LCD_I2C); 

Аналогично объявлен и используемый дисплей — задан его тип.

Инициализация сенсоров и дисплея происходит в методе setup:

void setupSensors() {     outputDatas = new SensorOutputData[sensorsCount];     sensorEntities = new SensorEntity*[sensorsCount];     for (int i = 0; i < sensorsCount; i++)     {         int sensorType = sensorTypes[i];         int pin = sensorPins[i];         SensorEntity* entity = new SensorEntity(sensorType);         entity->setup(pin);         sensorEntities[i] = entity;     } }  void setupDisplay() {     DisplayConfig displayConfig = DisplayConfig();     displayConfig.address = 0x27;     displayConfig.rows = 2;     displayConfig.cols = 16;     displayConfig.sda = 4;     displayConfig.scl = 5;     displayConfig.printSensorTitle = false;          display.setup(displayConfig);     display.clear(); } 

На каждом цикле замера происходит получение данных с сенсоров и передача их на дисплей для отображения:

void requestSensorValues() {     for (int i = 0; i < sensorsCount; i++)     {         SensorEntity* entity = sensorEntities[i];         SensorOutputData sensorData = entity->getData();         sensorData.sensorOrder = i;         outputDatas[i] = sensorData;     } }  void renderSensorValues() {     for (int i = 0; i < sensorsCount; i++)     {         SensorOutputData sensorData = outputDatas[i];         display.printData(sensorData);     } } 

Собственно, эти небольшие куски кода — и могли бы быть всей программой, если бы не работа с вай-фай или создание точки доступа на esp8266 для настройки модулей.
Как видно, если пользователю нужно поменять используемый сенсор, или подключить новый дисплей — это будет сделать очень просто: потребуется всего-лишь прописать их типы, и поменять конфигурацию пинов.

Если понадобится добавить новый тип сенсора или дисплея — то следует создать наследника от базового класса, и переопределить методы получения данных и их отрисовки.

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

3. При отрисовке графиков на сайте теперь используется фильтр Калмана, чтобы убрать дребезг значений.

4. Теперь есть возможность управлять видимостью и названиями сенсоров на модулях — это можно сделать на странице настроек каждого модуля на сайте.

5. Основная возможность, добавленная в этой ревизии метеостанции — это поддержка работы с датчиками Aqara для экосистемы умного дома от Xiaomi.

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

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

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

Приведу код модуля и конфига — см. под катом:

Модуль и конфиг на nodejs

Модуль:

var request = require("request"); var config = require('./config');  const dgram = require('dgram'); const serverPort = config.serverPort; const serverSocket = dgram.createSocket('udp4'); const multicastAddress = config.multicastAddress; const multicastPort = config.multicastPort; const sensorDelay = config.sensorDelay;  var sidToAddress = {}; var sidToPort = {}; var gatewayAddress;  function sendSensorData(sensorId, temperature, humidity, gatewayAddress) {     request({             url: config.addDataUrl,             method: 'GET',             qs: {                 isaqara: 1,                 moduleid: sensorId,                 modulename: sensorId,                 code: config.validationCode,                 temperature1: temperature,                 humidity1: humidity,                 ip: gatewayAddress,                 mac: sensorId,                 delay: sensorDelay             }         },         function (error, response, body) {             if (error) {                 console.log(error);             }         }     ); }  serverSocket.on('message', function (msg, rinfo) {      console.log('Received x1b[33m%sx1b[0m (%d bytes) from client x1b[36m%s:%dx1b[0m.', msg, msg.length, rinfo.address, rinfo.port);     var json;     try {         json = JSON.parse(msg);     }     catch (e) {         console.log('x1b[31mUnexpected message: %sx1b[0m.', msg);         return;     }      var cmd = json['cmd'];      if (cmd === 'iam') {          var address = json['ip'];         var port = json['port'];          gatewayAddress = address;          var command = {             cmd: "get_id_list"         };         var cmdString = JSON.stringify(command);         var message = new Buffer(cmdString);         serverSocket.send(message, 0, cmdString.length, port, address);          console.log('Requesting devices list...');     }     else if (cmd === 'get_id_list_ack') {          var data = JSON.parse(json['data']);         console.log('Received devices list: %d device(s) connected.', data.length);         for (var index in data) {             var sid = data[index];             var command = {                 cmd: "read",                 sid: new String(sid)             };              sidToAddress[sid] = rinfo.address;             sidToPort[sid] = rinfo.port;              var cmdString = JSON.stringify(command);             var message = new Buffer(cmdString);             serverSocket.send(message, 0, cmdString.length, rinfo.port, rinfo.address);             console.log('Sending x1b[33m%sx1b[0m to x1b[36m%s:%dx1b[0m.', cmdString, rinfo.address, rinfo.port);         }     }     else if (cmd === 'read_ack' || cmd === 'report' || cmd === 'heartbeat') {          var model = json['model'];         var data = JSON.parse(json['data']);          if (model === 'sensor_ht') {             var temperature = data['temperature'] ? data['temperature'] / 100.0 : 100;             var humidity = data['humidity'] ? data['humidity'] / 100.0 : 0;             var sensorId = json["short_id"];              console.log("Received data from sensor x1b[31m%sx1b[0m (sensorId: %s) data: temperature %d, humidity %d.", json['sid'], sensorId, temperature, humidity);              sendSensorData(sensorId, temperature, humidity, gatewayAddress);              console.log('Sending sensor data to x1b[36m%sx1b[0m.', config.addDataUrl);         }     } });  // err - Error object, https://nodejs.org/api/errors.html serverSocket.on('error', function (err) {     console.log('Error, message - %s, stack - %s.', err.message, err.stack); });  serverSocket.on('listening', function () {     console.log('Starting a UDP server, listening on port %d.', serverPort);     serverSocket.addMembership(multicastAddress); })  console.log('Starting Aqara daemon...');  serverSocket.bind(serverPort);  function sendWhois() {     var command = {         cmd: "whois"     };     var cmdString = JSON.stringify(command);     var message = new Buffer(cmdString);     serverSocket.send(message, 0, cmdString.length, multicastPort, multicastAddress);     console.log('Sending WhoIs request to a multicast address x1b[36m%s:%dx1b[0m.', multicastAddress, multicastPort); }  sendWhois();  setInterval(function () {     console.log('Requesting data...');     sendWhois(); }, sensorDelay * 1000); 

Конфиг:

var config = {     validationCode: "0000000000000000",     addDataUrl: "http://weatherhub.ru/aqara.php",     serverPort: 9898,     multicastAddress: '224.0.0.50',     multicastPort: 4321,     sensorDelay: 30 };  module.exports = config; 

Для запуска модуля на Малинке — используется команда nodejs sensor.js
Как автоматизировать запуск модуля при старте Малинки — пока не решил, вероятно кто-нибудь подскажет в комментариях, как это сделать проще и красивее.

Вид получаемых данных из консоли Малинки:

Получаемые данные

root@raspberrypi:/home/nodejs# nodejs sensor.js Starting Aqara daemon... Sending WhoIs request to a multicast address 224.0.0.50:4321. Starting a UDP server, listening on port 9898. Received {"cmd":"iam","port":"9898","sid":"f0b429cc178e","model":"gateway","ip":"192.168.1.112"} (87 bytes) from client 192.168.1.112:4321. Requesting devices list... Received {"cmd":"get_id_list_ack","sid":"f0b429cc178e","token":"GVke0tYsRZ5zlXWc","data":"["158d00015b2f98","158d0001560c23","158d00013eccc6","158d000153db73","158d000127883b","158d0001581523","158d0001101d54"]"} (217 bytes) from client 192.168.1.112:9898. Received devices list: 7 device(s) connected. Sending {"cmd":"read","sid":"158d00015b2f98"} to 192.168.1.112:9898. Sending {"cmd":"read","sid":"158d0001560c23"} to 192.168.1.112:9898. Sending {"cmd":"read","sid":"158d00013eccc6"} to 192.168.1.112:9898. Sending {"cmd":"read","sid":"158d000153db73"} to 192.168.1.112:9898. Sending {"cmd":"read","sid":"158d000127883b"} to 192.168.1.112:9898. Sending {"cmd":"read","sid":"158d0001581523"} to 192.168.1.112:9898. Sending {"cmd":"read","sid":"158d0001101d54"} to 192.168.1.112:9898. Received {"cmd":"read_ack","model":"sensor_ht","sid":"158d00015b2f98","short_id":20046,"data":"{"voltage":2975,"temperature":"2297","humidity":"4190"}"} (153 bytes) from client 192.168.1.112:9898. Received data from sensor 158d00015b2f98 (sensorId: 20046) data: temperature 22.97, humidity 41.9. Sending sensor data to http://weatherhub.ru/aqara.php. Received {"cmd":"read_ack","model":"motion","sid":"158d0001560c23","short_id":41212,"data":"{"voltage":3075}"} (103 bytes) from client 192.168.1.112:9898. Received {"cmd":"read_ack","model":"switch","sid":"158d00013eccc6","short_id":4019,"data":"{"voltage":3042}"} (102 bytes) from client 192.168.1.112:9898. Received {"cmd":"read_ack","model":"magnet","sid":"158d000153db73","short_id":4914,"data":"{"voltage":3015,"status":"unknown"}"} (125 bytes) from client 192.168.1.112:9898. Received {"cmd":"read_ack","model":"plug","sid":"158d000127883b","short_id":52305,"data":"{"voltage":3600,"status":"unknown","inuse":"0"}"} (140 bytes) from client 192.168.1.112:9898. Received {"cmd":"read_ack","model":"sensor_ht","sid":"158d0001581523","short_id":52585,"data":"{"voltage":3035,"temperature":"2287","humidity":"4340"}"} (153 bytes) from client 192.168.1.112:9898. Received data from sensor 158d0001581523 (sensorId: 52585) data: temperature 22.87, humidity 43.4. Sending sensor data to http://weatherhub.ru/aqara.php. Received {"cmd":"read_ack","model":"switch","sid":"158d0001101d54","short_id":3344,"data":"{"voltage":3032}"} (102 bytes) from client 192.168.1.112:9898. Received {"cmd":"heartbeat","model":"gateway","sid":"f0b429cc178e","short_id":"0","token":"oypMd4l87xHIR6oP","data":"{"ip":"192.168.1.112"}"} (136 bytes) from client 192.168.1.112:4321. 

Как можно увидеть из вывода, в сети работает два датчика, с идентификаторами 52585 и 20046. Данные с них отправляются на сервер, указанный в конфиге (http://weatherhub.ru) — где поднят сам сайт и БД для хранения данных.

После запуска модуля, подключения датчиков, и запуска сайта — новые датчики можно будет сразу увидеть на странице Настройки:

Используя кнопку Параметры модуля — выбираем активные сенсоры (в нашем случае это Температура 1 и Влажность 1), сохраняем данные, и переходим на Главную, где видим получаемые данные:

На странице Данные — можно посмотреть данные с табличной форме:

На странице Графики — графики, для которых можно выбрать период отображения:

Сайт запущен в режиме одного пользователя, без авторизации. Поэтому отображаемые данные доступны всем. При включении авторизации — каждый пользователь получает уникальный код валидации, который надо будет указать либо в микропрограмме для контроллера, либо в модуле nodejs.

Весь код метеостанции доступен на Гитхабе: github.com/aproschenko-dev/WeatherHub

Дальнейшее развитие, которое мне видится:

  • Возможность показа данных в графическом виде, в виде шкалы
  • Добавление в стандартный набор поддерживаемых сенсоров наиболее распостраненных моделей — bme280, bmp280 и прочие
  • Поддержка датчика CO2 — MHT-Z19

От читателей — хотелось бы услышать в комментариях критику по делу, пожелания к системе, и — что было бы наиболее ценно — личный опыт развертывания и использования системы, в том числе с датчиками Aqara, которые стали стоить достаточно недорого.

Уверен, что при развертывании и запуске метеостанции могут возникнуть множество вопросов — готов на них дать ответы, а по итогам — написать подробный мануал по системе.

 
Источник

Читайте также