Рубрики
Железо Умный дом

MQTT Энергометр V.1

Умный дом — понятие растяжимое, обширное. Эта статья станет первой в моём блоге на эту тему. Здесь, мы рассмотрим одно из устройств, предназначенное для облегчения контроля, в частности контроля энергопотребления. Устройство по сути несложное, но очень информативное.

Одним из назначений «умного дома», является экономия электроэнергии. Но как её экономить, если мы не знаем сколько потребляем?! Для этих целей, я собрал устройство и известных и доступных компонентов, которые можно купить на Aliexpress.

Задача энергометра

Автономно, круглосуточно собирать измерения электрических характеристик входящей линии электропередач в квартире или доме. После чего, посредством беспроводного канала Wi-Fi и протокола MQTT передавать актуальные данные на сервер — хаб умного дома.

Состав энергометра

  1. Основной и пожалуй самой сложной частью, является электрический счётчик PZEM-004T. Он умеет измерять текущее напряжение переменного тока от 80 до 260 В. Текущую силу тока от 0 до 100 А. Основываясь на двух предшествующих измерениях, вычислять потребляемую мощность в Ваттах. А так же сохранять в своей памяти потреблённые ватты за единицы времени — по сути счётчик. В микросхеме устройства на самом деле больше функций, но ни одна другая нам не понадобится.
  2. Микроконтроллер ESP 8266 ESP-01, в качестве опрашивающего, корректирующего данные и передающего устройства. В этой версии (V1) я использовал только выводы «RX/TX» для общения со счётчиком. Выводы «GPIO 0» и «GPIO 2» остались свободны. Их можно использовать для управления, но об этом в конце статьи.
  3. Адаптер питания. Вход ~ 220 В, выход = 5 В и = 3.3 В. Перед адаптером я поместил ферритовый фильтр с тороидальной намоткой, для защиты от побочных электромагнитных излучений и наводок.
  4. Обвязка, для соединения всех компонентов воедино. Кроме необходимых соединений, таких как питание и информационные шины, не стоит забывать, что ESP-01 имеет недостаток на выведенных пинах. Так, например, между «GND» и «VCC» желательно установить керамический конденсатор на 0,1 мкФ для фильтрации высокочастотных помех. Выводы «RESET», «CH_PD» и «GPIO 0» нужно подтянуть к «VCC» через маломощные резисторы 10 кОм (можно заменить на 4,7 кОм). Выводы «RX/TX» в данном, конкретном случае соединены со счётчиком посредством оптронов находящихся на плате счётчика. Ещё один, не менее важный момент, оптроны используются для гальванической развязки, но логические уровни там 5 вольт, а на ESP-8266 3.3 вольта. Поэтому, нужно согласовать уровни с помощью резистора 1 кОм (смотри на фото ниже).
  5. Корпус должен быть надёжным. Защищать наше устройство от агрессивной внешней среды, например от пыли, брызг и посторонних предметов. А так же защищать место установки от гипотетических проблем с нашим устройств. Нужно помнить, что здесь мы имеем дело с опасным напряжением.
PZEM logic level fix

Установка

Мне нужно было измерять потребление всей квартиры поэтому, я просто установил Энергометр рядом со штатным счётчиком, прямо на лестничной площадке в электрическом щите. Правда он у нас закрывается на замок, а то мало ли что… Корпус прикрепил к каркасу щита с помощью нейлоновых стяжек. Фазовый провод идущий от штатного счётчика, аккуратно вытащил из защитных автоматов. Продел его через соленойд (токовый трансформатор) и вернул его обратно. Питание Энергометра подключил к общему «нулю» и тому же фазовому проводу (это важно для точности измерений) с помощью «крокодильчиков».

Интеграция с Home Assistant

Добавляем 4 новых сенсора типа MQTT. Адреса топиков конечно можно изменить на свои, главное чтобы они совпадали с прошивкой. Мой рабочий фрагмент конфигурации Home Assistant:

- platform: mqtt
  name: "Voltage"
  state_topic: "home/energymeter/v"
  availability_topic: "home/energymeter/status"
  unit_of_measurement: "V"
  value_template: '{{(value | int)}}'
- platform: mqtt
  name: "Current"
  state_topic: "home/energymeter/i"
  availability_topic: "home/energymeter/status"
  unit_of_measurement: "A"
  value_template: '{{(value | float)}}'
- platform: mqtt
  name: "Power"
  state_topic: "home/energymeter/p"
  availability_topic: "home/energymeter/status"
  unit_of_measurement: "W"
  value_template: '{{(value | int)}}'
- platform: mqtt
  name: "Energy"
  state_topic: "home/energymeter/e"
  availability_topic: "home/energymeter/status"
  unit_of_measurement: "Wh"
  value_template: '{{(value | int)}}'

Исходный код прошивки Энергометр V.1

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <PubSubClient.h>
#include <PZEM004T.h>

PZEM004T pzem(&Serial);
IPAddress ip(192,168,1,1);

// ---------- Configuration ----------

// WiFi
#define wifiSSID "*****"
#define wifiPASS "*****"

// MQTT
#define mqttUser "*****"
#define mqttPass "*****"
#define mqttTopicPrefix "home/energymeter"

char mqttTopicState[64];
char mqttTopicStatus[64];
char mqttTopicIp[64];
char mqttTopicV[64];  // voltage
char mqttTopicI[64];  // amperage
char mqttTopicP[64];  // power
char mqttTopicE[64];  // energy

char hostString[16];
IPAddress brokerIp;
uint16_t brokerPort;
char curIp[16];

long lastReconnectAttempt = 0; // For the non blocking mqtt reconnect (in millis)
long lastSendData = 0; // Последняя отправка

bool pzemok = false;
float vcache = 0.0;
float icache = 0.0;
float pcache = 0.0;
float ecache = 0.0;

WiFiClient espClient;
PubSubClient client(espClient);

// ---------- SETUP ---------- //

void setup() {
   while (!pzemok) {
      pzemok = pzem.setAddress(ip);
      delay(1000);
   }

  sprintf(hostString, "ESP_%06X", ESP.getChipId());

  // put in mqtt prefix
  sprintf(mqttTopicStatus, "%s/status", mqttTopicPrefix);
  sprintf(mqttTopicIp,     "%s/ip",     mqttTopicPrefix);
  sprintf(mqttTopicV,      "%s/v",      mqttTopicPrefix);
  sprintf(mqttTopicI,      "%s/i",      mqttTopicPrefix);
  sprintf(mqttTopicP,      "%s/p",      mqttTopicPrefix);
  sprintf(mqttTopicE,      "%s/e",      mqttTopicPrefix);

  setup_wifi();

  if (!find_mqtt_broker()) { ESP.restart(); }
  
  client.setServer(brokerIp, brokerPort);
  client.setCallback(mqttCallback);
  
}

void setup_wifi() {
  delay(10);

  WiFi.mode(WIFI_STA);
  WiFi.hostname(hostString);
  WiFi.begin(wifiSSID, wifiPASS);

  while (WiFi.status() != WL_CONNECTED) { delay(500); }

  sprintf(curIp, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]);
}

/* Discover raspberry using MDNS */
bool find_mqtt_broker() {
  if (!MDNS.begin(hostString)) {
    return false;
  } else {
    delay(10);

    /* Start discovery of MDNS (e.g. avahi) service advertisements for MQTT */
    int n = -1;
    do {
      if (!n) { delay(1000); }
      n = MDNS.queryService("mqtt", "tcp");
    } while (n == 0);
    brokerIp = MDNS.IP(0);
    brokerPort = MDNS.port(0);
    return true;
  }
}


bool mqttReconnect() {
  if (!client.connected()) {

    // Attempt to connect with last will retained
    if (client.connect(hostString, mqttUser, mqttPass, mqttTopicStatus, 1, true, "offline")) {

      client.publish(mqttTopicStatus, "online", true);
      client.publish(mqttTopicIp, curIp, true);

      // ... and (re)subscribe
//      client.subscribe(mqttTopicDo);

    }
  }
  return client.connected();
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
// тут пусто
}

void sendData() {
  float v = pzem.voltage(ip);
  if (v > 0.0) { // Нам нужны корректные данные
   if (v < vcache - 1.0 || v > vcache + 1.0) { // Избавляемся от "шума"
    String v_string = String(v);
    char v_char[8];
    v_string.toCharArray( v_char, 8 );
    client.publish(mqttTopicV, v_char, true);
    vcache = v;
   }    
  } else { delay(1000); } // Счётчик не успел отдать данные

  float i = pzem.current(ip);
  if (i > 0.0) {
   if (i < icache - 0.1 || i > icache + 0.1) {
    String i_string = String(i);
    char i_char[8];
    i_string.toCharArray( i_char, 8 );
    client.publish(mqttTopicI, i_char, true);
    icache = i;
   }
  } else { delay(1000); }
  
  float p = pzem.power(ip);
  if (p > 0.0) {
   if (p < pcache - 1.0 || p > pcache + 1.0) {
    String p_string = String(p);
    char p_char[8];
    p_string.toCharArray( p_char, 8 );
    client.publish(mqttTopicP, p_char, true);
    pcache = p;
   }
  } else { delay(1000); }
  
  float e = pzem.energy(ip);
  if (e > 0.0) {
   if (e < ecache - 1.0 || e > ecache + 1.0) {
    String e_string = String(e);
    char e_char[8];
    e_string.toCharArray( e_char, 8 );
    client.publish(mqttTopicE, e_char, true);
    ecache = e;
   }
  } else { delay(1000); }

}

  

// ---------- LOOP ----------
void loop() {

  /* Обрабатка соединения mqtt (исключает блокировку брокером) */
  if (!client.connected()) {
    long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      if (mqttReconnect()) { lastReconnectAttempt = 0; } // Попытка повторного подключения
    }
  } else { 
    long now = millis();
    if (now - lastSendData > 3000) { // Отправка через каждые 3 сек.
      lastSendData = now;
      sendData();
    }  
    client.loop();
  }

  yield(); // Задержка для корректной работы WiFi
}

Исходный код публикую на условиях лицензии: «WTFPL – Do What the Fuck You Want to Public License»

Замечания и улучшения

В следующей версии, я планирую изменить и добавить вот что:

  • Нужен предохранитель, желательно самовосстанавливающийся!
  • Резервное питание для ESP-01. Так как при отключении «света» сенсор отключится и мы не узнаем об истинной причине. Возможно имеет смысл, кроме предохранителя, использовать нормально замкнутое реле на оба провода (фаза и нуль), а управлять им через свободный GPIO.
  • PZEM-004T имеет возможность сбросить счётчик использованной электроэнергии. Для этого есть кнопка на плате. Было бы полезным один из GPIO от ESP-01 использовать для удалённого сброса.
  • Температурный датчик не кажется лишним.

Спустя год

Энергометр всё ещё работает исправно. Однако я сменил прошивку на Sonoff-Tasmota. Возможности добавились, рекомендую!

Автор: Илья Балдуев

Специалист ИТ, веб программист, фотограф, блоггер. Добрый, но злопамятный. Верный, но не Хатико. Честный, не всегда. Упрямый, но ленивый.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *