Привет всем.

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

КДПВ

КДПВ.

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

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

Железячная часть (на 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%s\x1b[0m (%d bytes) from client \x1b[36m%s:%d\x1b[0m.', msg, msg.length, rinfo.address, rinfo.port);
    var json;
    try {
        json = JSON.parse(msg);
    }
    catch (e) {
        console.log('\x1b[31mUnexpected message: %s\x1b[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%s\x1b[0m to \x1b[36m%s:%d\x1b[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%s\x1b[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%s\x1b[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:%d\x1b[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, которые стали стоить достаточно недорого.

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

Комментарии (27)


  1. velovich
    19.12.2017 22:40

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


    1. Alkop
      19.12.2017 23:36

      Если надо готовое, качественное решение, то наверно вам подойдёт Netatmo.


      1. velovich
        19.12.2017 23:48

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


        1. Rumlin
          20.12.2017 09:35

          взрослым метеослужбам необходимы достоверные данные. Подоконник к такому классу площадок и устройств не относится.


        1. igrushkin
          20.12.2017 09:42

          WeatherUnderground на этом и построена, на сети из метеостанций энтузиастов


          1. safari2012
            20.12.2017 17:21

            Не только построена, но ещё существует классная opensource DYI-метеостанция на ESP+экранчике, на базе этого сервиса.


            1. igrushkin
              20.12.2017 17:25

              не путайте причину и следствие. Я говорил про ИСТОЧНИК данных для WU, а Вы используете WU в КАЧЕСТВЕ источника


              1. safari2012
                20.12.2017 17:39

                Я то не путаю, т.к. использую и то и другое :)


        1. vin2809
          20.12.2017 14:42

          А как же хобби? Или Вы все свободное время смотрите на термометры или в телевизор?

          Честь, хвала и уважение автору.

          Человек описал пример своего времяпровождения, если имеете что-то против, опишите и Вы как проводите свое свободное время…


        1. Alkop
          20.12.2017 17:27

          Извините. А www.wunderground.com является взрослой?
          Потому что они используют станции Netatmo в своих картах-прогнозах.


  1. pokryshkin
    20.12.2017 03:53

    Поддержка новых датчиков: возьмите за основу ESPEasy. Ставится и настраивается буквально в два клика. Поддерживает кучу датчиков и исполнительных устройств. Сразу из коробки подключается к Domoticz, OpenHab, MQTT и т.д., если хотите свой протокол — напишите «плагин».


    1. alexpp Автор
      20.12.2017 17:58

      Целью проекта было, в первую очередь, упростить написание кода под Ардуино-Esp — без сторонних прошивок, и второе — научиться получать данные с Aqara, и отправлять их для хранения-обработки. Максимальный охват датчиков в проекте — и не планировался, так как проект задуман как концепт, где каждый сможет элементарно добавить свой датчик. Естественно, при необходимости, как автор проекта — смогу помочь и код уйдет в репозиторий.


      1. geisha
        20.12.2017 23:36

        Целью проекта было, в первую очередь, упростить написание кода под Ардуино-Esp — без сторонних прошивок

        Ну вы изобрели велосипед. Вы подразумеваете, что писать в Arduino IDE — достаточно сложное занятие, но ИМХО это и есть как раз самый простой способ. Я не понял, что вы имеете ввиду под "сторонними прошивками" (может, проект LUA для ESP), но, опять же, Arduino IDE заливает много чего стороннего в ESP. Самый тонкий контроль ESPшки — через заводской API — но и он не избавляет от бинарных блобов от производителя. В конце концов, гугл выдаёт 54,000 результатов на "esp weather station github". Плюс есть готовые решения, такие, как предложили выше или IOT-based, для которых вообще писать код не надо: просто цепляешь датчики и настраиваешь. Да и вообще, к малинке можно много чего подключить напрямую: Aqara+ESP+Raspberry это какой-то оверкилл.


        Т.е. с самообразованием вы отлично справились, но практическая польза неочевидна.


        1. alexpp Автор
          21.12.2017 02:29

          Вы внимательно читали статью и смотрели код? Я нигде не говорил, что писать под Arduino-IDE — сложно. В статье сказано, что при большом количестве датчиков программа превращается в нечитаемую простыню кода, в которой становится трудно ориентироваться и что-либо понимать. Поэтому предложен ООП-подход, значительно сокращающий количество кода, увеличивающий читаемость, и позволяющий легко расширять возможности программы.


  1. smart_alex
    20.12.2017 10:12

    Из статьи не совсем понял о взаимодействии с датчиками Xiaomi. Было бы неплохо если бы кто-нибудь написал статью и подробно осветил вопрос как можно прикручивать датчики и прочее оборудование Xiaomi к своим поделкам: теория, протоколы, практические примеры и т. п.


    1. tmin10
      20.12.2017 10:15

      У хаба есть API, ключ можно получитьв приложении. Именно через него работает интеграция с OpenHAB. Хотя внятной документации от производителя мне найти не удалось (может только на китайском есть), только ковыряния в протоколе от интузиастов.


      1. smart_alex
        20.12.2017 10:26

        В идеале хотелось бы просто купить датчик Xiaomi и использовать его со своим железом и софтом, но такой фокус скорее всего не получится (хотя почему? — это было бы выгодно производителю — армия DIY-щиков во много раз подняла бы продажи датчиков). Но если это невозможно, то всё равно хотелось бы почитать статью про всю эту кухню оборудования Xiaomi и способах стороннего взаимодействия с ней.


        1. tmin10
          20.12.2017 13:08

          Датчики работают на протоколе Zigbee, так что чисто теоритически можно подцепить их к своему хабу.


        1. igrushkin
          20.12.2017 17:26

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


    1. alexpp Автор
      20.12.2017 14:57

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


    1. Visphord
      20.12.2017 16:38

      github.com/monster1025/aqara-mqtt — мини-сервис, который публикует всю информацию с датчиков xiaomi(aqara) в MQTT и обратно передает в xiaomi gateway.
      Легко интегрируется с любой «поделкой», публиковать\читать MQTT умеет даже arduino.


  1. Mogwaika
    20.12.2017 15:21

    У акары неприятная особенность, я надеялся, что будет работать на улице, но нет, на -20 показания градусника залипают (хотя возможно это проблема библиотеки home assistant-а).


    1. alexpp Автор
      20.12.2017 16:13

      Не проверял. Попробую запихнуть датчик в морозилку!


    1. Visphord
      20.12.2017 16:40

      у меня пока до -5 — живые. Возможно батарея на -20 просаживается до «отключения» датчика.


      1. Mogwaika
        21.12.2017 12:04

        Нет, он вроде даже влажность измеряет по разному и давление, а температуру константой выдаёт. Батарейка хоть и заметно садится, но градусник её уровень отдаёт.


    1. Visphord
      20.12.2017 19:43

      Провел эксперимент датчик (старый — только температура и влажность), шлюз xiaomi v2, гейт в mqtt, холодильник с температурой «на дисплее» -23.

      Начало эксперимента (темп. 10.68, вольтаж. 2.965в):

      Данные
      home/sensor_ht/living/humidity 1116
      home/sensor_ht/living/temperature 1068
      home/sensor_ht/living/voltage 2965
      


      1. Mogwaika
        21.12.2017 12:13

        Проверю на днях, что родное приложение показывало. Т.к. батарейка таки села (на морозе примерно за месяц с 50% начального заряда) и он выключился, а со второго телефона приложение показывает последнее значение -100 градусов и 650% влажности и график не открывает ))