Альтернативная прошивка для sonoff и управления Livolo

  • Цена: $4.85 + Доставка $4.61

Update: Добавлено видео

Рассказывать подробно о самом решении для управления силовой нагрузкой от Ited под названием Sonoff RF, я не вижу смысла, так как на mysku уже существует подобный обзор от пользователя spc. В данном обзоре, мне бы хотелось затронуть практическую сторону на примере реализации управления выключателем Livolo. Если кому-то интересна подобная тематика, прошу под кат.

Хотелось бы сразу отметить, что, не смотря на очевидные плюсы sonoff, типа готового аппаратно-программного решения в корпусе и народного Wifi модуля ESP8266 внутри, есть очень жирный минус — это закрытая прошивка и привязка к облачным сервисам Itead. Не нужно быть специалистом в области информационной безопасности, чтобы осознать какие могут возникнуть риски. Чтобы не изобретать велосипед и упростить выполнение поставленной задачи, было решено использовать прошивку с открытым исходным кодом от Theo Arends Sonoff-MQTT-OTA-Arduino.

Что нам потребуется

+ Itead Sonoff;
± Любой USB-UART переходник FTDI32RL, Cp2102 и т.д.*;
+ Wifi роутер в режиме точки доступа;
+ Локальный или облачный MQTT сервер;
± Паяльник, припой, флюс, штырьковый разъем и некоторые навыки пайки*.

* Можно использовать Веб-сервер и прошивку по воздуху (OTA: Over-the-air)

Подключаем sonoff через USB-UART


Для прошивки по USB-UART припаиваем штырьки



Внимание! Перед прошивкой по USB-UART отключите AC питания sonoff


У меня в наличии есть только переходник, основанный на микросхеме CP2102 от SILICON LABS, обзор на который уже были на mysku.



Sonoff RX -> TX UART
Sonoff TX -> RX UART
Sonoff VCC -> 3.3V UART
Sonoff GND -> GND UART

Установка и настройка ESP8266 Arduino IDE

Внимание!!! Возможны проблемы при компилации скетчей на Windows XP единственное известное мне, работающее решение — это использование Portable версии Arduino IDE.

Пример ошибки
c:\documents and settings\USERNAME\local settings\application data\arduino15\packages\esp8266\tools\xtensa-lx106-elf-gcc\1.20.0-26-gb404fb9-2\xtensa-lx106-elf\include\c++\4.8.2\bits\stl_algobase.h:59:28: fatal error: bits/c++config.h: No such file or directory

 #include <bits/c++config.h>
                       
compilation terminated.

exit status 1


Порядок действий
1. Скачиваем архив с Arduino 1.6.8+ с официального веб-сайта
2. Распаковываем архив в папку, например ESP8266_Arduino
3. Создаем в папке ESP8266_Arduino, папку Portable
4. Запускаем Arduino и открываем окно с настройками Файл->Настройки (File->Preferences);
5. Вводим arduino.esp8266.com/stable/package_esp8266com_index.json в Дополнительные ссылки для менеджера плат (Additional Board Manager URLs field).
Вы можете использовать несколько URL'ов, разделенных запятыми;
6. Окрываем окно с менеджером плат Иструменты->Плата:*->Менеджер плат (Tools->Board:*->Board Manager) и устанавливаем платформу esp8266
7. Копируем директорию sonoff в Ваш sketchfolder посмотреть путь можно в Файл->Настройки->Размещение папки скетчей (File->Preferences)
8. Скачиваем и распаковываем pubsubclient MQTT library в директорию portable\sketchbook\libraries. Переименовываем pubsubclient-master в pubsubclient и редактируем следующий файл pubsubclient\src\PubSubClient.h

Изменяем MQTT_MAX_PACKET_SIZE с 128 на 1024
Изменяем MQTT_KEEPALIVE с 15 на 120

Компиляция и загрузка прошивки


Для sonoff выбираем Инструменты->Плата:*->Generic ESP8266 Module (Tools->Board:*->Generic ESP8266 Module) и устанавливаем следующие настройки:


    Flash Mode: QIO
    Flash Frequency: 40MHz
    Upload Using: Serial
    CPU Frequency: 80MHz
    Flash Size: 1M (64K SPIFFS)
    Debug Port: Disabled
    Debug Level: None
    Reset Method: ck
    Upload Speed: 115200
    Port: Ваш COM-порт к которому подключен sonoff

Открываем sonoff.ino и изменяем параметры подключения к Wifi и MQTT серверу.

Название вашего проекта, если планируется использовать множество sonoff устройств, то меняем, например на sonoff1, sonoff2… sonoffx
#define PROJECT                "sonoff"



Задаем имя и пароль от вашего Wifi-роутреа\точки, к которой sonoff будет подключаться.
// Wifi
#define STA_SSID               ""
#define STA_PASS               ""


Вывод полезной для отладки информации в SERIAL
#define SERIAL_LOG_LEVEL       LOG_LEVEL_DEBUG_MORE


«Прошивка по воздуху» меняем адрес на свой HTTP сервер и копируем файлы, если не требуется можно оставить, как есть по умолчанию.
// Ota
#if (ARDUINO >= 168)
  #define OTA_URL              "http://192.168.0.102:80/api/arduino/"PROJECT".ino.bin"
#else
  #define OTA_URL              "http://192.168.0.102:80/api/arduino/"PROJECT".cpp.bin"
#endif

Адрес MQTT сервера и порт
// MQTT
#define MQTT_HOST              "192.168.0.102"
#define MQTT_PORT              1883
...


Пользователь и пароль для подключения к MQTT серверу
#define MQTT_USER              ""
#define MQTT_PASS              ""


Перед подключением USB-UART к порту компьютера, зажмите и удерживайте кнопку на sonoff, после подключения нажмите кнопку Загрузка в Arduino IDE, когда на экране появится сообщение о прошивке и появится прогресс бар, кнопку можно отпускать.



Использование

Проверим работоспособность, кратковременно нажимаем кнопку на sonoff, светодиод должен моргнуть дважды и устройство отправит сообщение «on» в топик stat/sonoff/POWER

Открываем монитор порта


RTC: sntp 0, Thu Jan 01 00:00:00 1970

APP: Multipress 1
MQTT: sonoff/LIGHT = 2
MQTT: Receive topic cmnd/sonoff/LIGHT, data 2
MQTT: DataCb Topic sonoff, Group 0, Type LIGHT, data 2 (2)
MQTT: sonoff/LIGHT = On
Config: Saved configuration to flash at F9 and count 38


Проверим работу sonoff с MQTT сервером, запускаем любой mqtt клиент на компьютере (MQTT Spy) или мобильном телефоне (MyMQTT), подключаемся и публикуем запись off в топик cmnd/sonoff/LIGHT



В Мониторе COM-порта:
Wifi: Check connection
MQTT: Receive topic cmnd/sonoff/LIGHT, data off
MQTT: DataCb Topic sonoff, Group 0, Type LIGHT, data off (OFF)
MQTT: sonoff/LIGHT = Off
RTC: sntp 0, Thu Jan 01 00:00:00 1970


В топике stat/sonoff/LIGHT должно появится сообщение с контентом Off.

Полный список команд, которые поддерживает данная прошивка, можно найти на странице проекта в Github

Доработка

У ESP8266 в sonoff есть свободные GPIO, которые можно использовать для подключения дополнительных модулей (Радиопередатчик, различные датчики и.т.д.).

В качестве примера подключим радиопередатчик RF433 и добавим в прошивку, новую команду LIVOLO для управления выключателями.

Список свободных и легкодоступных GPIO:
— GPIO1 — TX UART
— GPIO3 — RX UART
— GPIO12 — Красный светодиод
— GPIO13 — Зелёный светодиод, смело можно подключить второе реле
— GPIO14 — Пятый вывод возле UART (Только в новых ревизиях платы)

У моей sonoff, есть выведенный GPIO14, поэтому я буду подключать к нему. Очень интересный вариант, использовать отверстия под радиоприемник для подключения радиопередатчика, кому будет интересно оставляю ссылку.

Для отправки команд выключателям Livolo воспользуемся соответствующей библиотекой. Скачиваем и копируем папку в libraries.

Внимание код представлен для ознакомления и очень далек от идеала, например вместо substring, лучше использовать строковые функции C++.

Исправленная прошивка с добавлением команды Livolo.
/*
 * Sonoff and Wkaku by Theo Arends
 *
 * ESP-12F connections (Wkaku)
 * 3V3                                                     5V
 *                   |-------------------|       |---------|
 *  |                |   -------------   |    |1N4001|  |Relay|
 *  |                | -|          Tx |- |       |---------|
 *  |                | -|          Rx |- |                /
 *  |-------------------| En          |- |---| 1k|------|<  BC547B
 *  |                | -|             |-                  \
 *  |                | -|        IO00 |------|Switch|------|
 *  |                ---| IO12   IO02 |--- LED (ESP-12E/F) |
 *  |---| 1k|---|LED|---| IO13   IO15 |------|10k|---------|
 *  |-------------------| Vcc     Gnd |--------------------|
 *                       -------------                     |
 *                        | | | | | |                     Gnd
*/

#define PROJECT                "sonoff"
#define VERSION                0x01000C00   // 1.0.12
#define CFG_HOLDER             0x20160520   // Change this value to load default configurations

// Wifi
#define STA_SSID               ""
#define STA_PASS               ""
#define WIFI_HOSTNAME          "esp-%06x-%s"

// Syslog
#define LOG_LEVEL_NONE         0
#define LOG_LEVEL_ERROR        1
#define LOG_LEVEL_INFO         2
#define LOG_LEVEL_DEBUG        3
#define LOG_LEVEL_DEBUG_MORE   4

#define SYS_LOG_HOST           "sidnas2"
#define SYS_LOG_PORT           514
#define SYS_LOG_LEVEL          LOG_LEVEL_NONE
#define SERIAL_LOG_LEVEL       LOG_LEVEL_DEBUG_MORE

// Ota
#if (ARDUINO >= 168)
  #define OTA_URL              "http://192.168.0.102:80/api/arduino/"PROJECT".ino.bin"
#else
  #define OTA_URL              "http://192.168.0.102:80/api/arduino/"PROJECT".cpp.bin"
#endif

// MQTT
#define MQTT_HOST              ""
#define MQTT_PORT              1883

#define MQTT_CLIENT_ID         "DVES_%06X"  // Also fall back topic using Chip Id = last 6 characters of MAC address
#define MQTT_USER              ""
#define MQTT_PASS              ""

#define SUB_PREFIX             "cmnd"
#define PUB_PREFIX             "stat"
#define MQTT_GRPTOPIC          PROJECT"s"   // Group topic
#define MQTT_TOPIC             PROJECT

// Application
#define MQTT_SUBTOPIC          "POWER"
#define APP_TIMEZONE           1            // +1 hour (Amsterdam)
#define APP_POWER              0            // Saved power state Off

// End of user defines **************************************************************************

#define SERIAL_IO            // Enable serial command line
#define STATES                 10           // loops per second
#define MQTT_RETRY_SECS        10           // Seconds to retry MQTT connection

//#define LED_PIN                2            // GPIO 2 = Blue Led (0 = On, 1 = Off) - ESP-12
#define LED_PIN                13           // GPIO 13 = Green Led (0 = On, 1 = Off) - Sonoff
//#define LED_PIN                16           // NodeMCU
#define REL_PIN                12           // GPIO 12 = Red Led and Relay (0 = Off, 1 = On)
#define KEY_PIN                0            // GPIO 00 = Button
#define PRESSED                0
#define NOT_PRESSED            1

#define WIFI_STATUS            0
#define WIFI_SMARTCONFIG       1

#ifdef DEBUG_ESP_PORT
#define DEBUG_MSG(...) DEBUG_ESP_PORT.printf( __VA_ARGS__ )
#else
#define DEBUG_MSG(...) 
#endif

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <PubSubClient.h>
#include <livolo.h>

extern "C" uint32_t _SPIFFS_start;

struct SYSCFG {
  unsigned long cfg_holder;
  unsigned long saveFlag;
  unsigned long version;
  byte          seriallog_level;
  byte          syslog_level;
  char          syslog_host[32];
  char          sta_ssid[32];
  char          sta_pwd[64];
  char          otaUrl[80];
  char          mqtt_host[32];
  char          mqtt_grptopic[32];
  char          mqtt_topic[32];
  char          mqtt_topic2[32];
  char          mqtt_subtopic[32];
  int8_t        timezone;
  uint8_t       power;
} sysCfg;

struct TIME_T {
  uint8_t       Second;
  uint8_t       Minute;
  uint8_t       Hour;
  uint8_t       Wday;   // day of week, sunday is day 1
  uint8_t       Day;
  uint8_t       Month;
  char          MonthName[4];
  uint16_t      Year;
  unsigned long Valid;
} rtcTime;

char Version[16];
char Hostname[32];
uint8_t mqttcounter = 0;
unsigned long timerxs = 0, timersec = 0;
int state = 0;
int otaflag = 0;
int restartflag = 0;
int smartconfigflag = 0;
int heartbeatflag = 0;
int heartbeat = 0;

WiFiClient espClient;
PubSubClient mqttClient(espClient);
WiFiUDP portUDP;   // syslog

int blinks = 1;
uint8_t blinkstate = 1;

uint8_t lastbutton = NOT_PRESSED;
uint8_t holdcount = 0;
uint8_t multiwindow = 0;
uint8_t multipress = 0;


/*********************************************************************************************\
 * Livolo
\*********************************************************************************************/
Livolo livolo(14); // transmitter connected to pin #14

void sendCommand(String str) {  
  String sRemoteID, sKeyCode;
  int curIndex = str.indexOf(',');
  
  sRemoteID = str.substring(0, curIndex);
  sKeyCode = str.substring(curIndex +1, -1);

  if(sRemoteID.startsWith("RemoteID:")) {
    sRemoteID = sRemoteID.substring(sRemoteID.lastIndexOf(':') +1,-1);
  }

  if(sKeyCode.startsWith("KeyCode:")) {
    sKeyCode = sKeyCode.substring(sKeyCode.lastIndexOf(':') +1,-1);
  }

  int iRemoteID = sRemoteID.toInt();
  int iKeyCode = sKeyCode.toInt();

  livolo.sendButton(sRemoteID.toInt(), sKeyCode.toInt());
}


/*********************************************************************************************\
 * Syslog
\*********************************************************************************************/
void syslog(const char *message)
{
  char mess[168], str[200];

  portUDP.beginPacket(sysCfg.syslog_host, SYS_LOG_PORT);
  strncpy(mess, message, 167);
  mess[168] = 0;
  sprintf_P(str, PSTR("%s %s"), Hostname, mess);
  portUDP.write(str);
  portUDP.endPacket();
}

void addLog(byte loglevel, const char *line)
{
  DEBUG_MSG("DebugMsg %s\n", line);  
#ifdef SERIAL_IO
  if (loglevel <= sysCfg.seriallog_level) Serial.println(line);
#endif
  if ((WiFi.status() == WL_CONNECTED) && (loglevel <= sysCfg.syslog_level)) syslog(line);
}

void addLog(byte loglevel, String& string)
{
  addLog(loglevel, string.c_str());
}

/********************************************************************************************/

void mqtt_publish(const char* topic, const char* data)
{
  char log[300];
  
  mqttClient.publish(topic, data);
  sprintf_P(log, PSTR("MQTT: %s = %s"), strchr(topic,'/')+1, data);     // Skip topic prefix
  addLog(LOG_LEVEL_INFO, log);
  mqttClient.loop();  // Solve LmacRxBlk:1 messages
  blinks++;
}

void mqtt_connected()
{
  char stopic[40], svalue[40];

  sprintf_P(stopic, PSTR("%s/%s/#"), SUB_PREFIX, sysCfg.mqtt_topic);
  mqttClient.subscribe(stopic);
  mqttClient.loop();  // Solve LmacRxBlk:1 messages
  sprintf_P(stopic, PSTR("%s/%s/#"), SUB_PREFIX, sysCfg.mqtt_grptopic);
  mqttClient.subscribe(stopic);
  mqttClient.loop();  // Solve LmacRxBlk:1 messages
  sprintf_P(stopic, PSTR("%s/"MQTT_CLIENT_ID"/#"), SUB_PREFIX, ESP.getChipId());  // Fall back topic
  mqttClient.subscribe(stopic);
  mqttClient.loop();  // Solve LmacRxBlk:1 messages

  sprintf_P(stopic, PSTR("%s/%s/NAME"), PUB_PREFIX, sysCfg.mqtt_topic);
  sprintf_P(svalue, PSTR("Sonoff switch"));
  mqtt_publish(stopic, svalue);
  sprintf_P(stopic, PSTR("%s/%s/VERSION"), PUB_PREFIX, sysCfg.mqtt_topic);
  sprintf_P(svalue, PSTR("%s"), Version);
  mqtt_publish(stopic, svalue);
  sprintf_P(stopic, PSTR("%s/%s/FALLBACKTOPIC"), PUB_PREFIX, sysCfg.mqtt_topic);
  sprintf_P(svalue, PSTR(MQTT_CLIENT_ID), ESP.getChipId());
  mqtt_publish(stopic, svalue);
}

void mqtt_reconnect()
{
  char stopic[40], svalue[40], log[80];

  mqttcounter = MQTT_RETRY_SECS;
  addLog(LOG_LEVEL_INFO, "MQTT: Attempting connection");
  sprintf(svalue, MQTT_CLIENT_ID, ESP.getChipId());
  sprintf_P(stopic, PSTR("%s/%s/lwt"), PUB_PREFIX, sysCfg.mqtt_topic);
  if (mqttClient.connect(svalue, MQTT_USER, MQTT_PASS, stopic, 0, 0, "offline")) {
    addLog(LOG_LEVEL_INFO, "MQTT: Connected");
    mqttcounter = 0;
    mqtt_connected();
  } else {
    sprintf_P(log, PSTR("MQTT: Connect failed, rc %d. Retry in %d seconds"), mqttClient.state(), mqttcounter);
    addLog(LOG_LEVEL_DEBUG, log);
  }
}

void mqttDataCb(char* topic, byte* data, unsigned int data_len)
{
  int i, grpflg = 0;
  char *str, *p, *mtopic = NULL, *type = NULL;
  char stopic[40], svalue[240];

  int topic_len = strlen(topic);
  char topicBuf[topic_len +1]; 
  char dataBuf[data_len +1]; 
  char dataBufUc[data_len +1]; 
  
  memcpy(topicBuf, topic, topic_len);
  topicBuf[topic_len] = 0;

  memcpy(dataBuf, data, data_len);
  dataBuf[data_len] = 0;

  sprintf_P(svalue, PSTR("MQTT: Receive topic %s, data %s"), topicBuf, dataBuf);
  addLog(LOG_LEVEL_DEBUG, svalue);

  i = 0;
  for (str = strtok_r(topicBuf, "/", &p); str && i < 3; str = strtok_r(NULL, "/", &p)) {
    switch (i++) {
    case 0:  // cmnd
      break;
    case 1:  // Topic / GroupTopic / DVES_123456
      mtopic = str;
      break;
    case 2:  // Text
      type = str;
    }
  }
  if (!strcmp(mtopic, sysCfg.mqtt_grptopic)) grpflg = 1;
  if (type != NULL) for(i = 0; i < strlen(type); i++) type[i] = toupper(type[i]);

  for(i = 0; i <= data_len; i++) dataBufUc[i] = toupper(dataBuf[i]);

  sprintf_P(svalue, PSTR("MQTT: DataCb Topic %s, Group %d, Type %s, data %s (%s)"),
    mtopic, grpflg, type, dataBuf, dataBufUc);
  addLog(LOG_LEVEL_DEBUG, svalue);

  if (type != NULL) {
    sprintf_P(stopic, PSTR("%s/%s/%s"), PUB_PREFIX, sysCfg.mqtt_topic, type);
    strcpy(svalue, "Error");

    uint16_t payload = atoi(dataBuf);
    if (!strcmp(dataBufUc,"OFF")) payload = 0;
    if (!strcmp(dataBufUc,"ON")) payload = 1;
    if (!strcmp(dataBufUc,"TOGGLE")) payload = 2;

    if (!strcmp(type,"STATUS")) {
      switch (payload) {
      case 1:
        sprintf_P(svalue, PSTR("%s, "MQTT_CLIENT_ID", %s, %s, %d, %d"),
          sysCfg.mqtt_grptopic, ESP.getChipId(), sysCfg.otaUrl, sysCfg.mqtt_host, heartbeat, sysCfg.saveFlag);
        break;  
      case 2:
        sprintf_P(svalue, PSTR("Version %s, Boot %d, SDK %s"),
          Version, ESP.getBootVersion(), ESP.getSdkVersion());
        break;
      case 3:
        sprintf_P(svalue, PSTR("Seriallog %d, Syslog %d, LogHost %s, SSId %s, Password %s"),
          sysCfg.seriallog_level, sysCfg.syslog_level, sysCfg.syslog_host, sysCfg.sta_ssid, sysCfg.sta_pwd);
        break;
      case 4:
        sprintf_P(svalue, PSTR("Sketch size %d, Free %d (Heap %d), Spiffs start %d, Flash size %d (%d)"),
          ESP.getSketchSize(), ESP.getFreeSketchSpace(), ESP.getFreeHeap(), (uint32_t)&_SPIFFS_start - 0x40200000,
          ESP.getFlashChipRealSize(), ESP.getFlashChipSize());
        break;
      case 5: {
        IPAddress ip = WiFi.localIP();
        IPAddress gw = WiFi.gatewayIP();
        IPAddress nm = WiFi.subnetMask();
        sprintf_P(svalue, PSTR("Hostname %s, IP %u.%u.%u.%u, Gateway %u.%u.%u.%u, Subnetmask %u.%u.%u.%u"),
          Hostname, ip[0], ip[1], ip[2], ip[3], gw[0], gw[1], gw[2], gw[3], nm[0], nm[1], nm[2], nm[3]);
        break;
      }
      default:
        sprintf_P(svalue, PSTR("%s, %s, %s, %s, %d, %d"),
          Version, sysCfg.mqtt_topic, sysCfg.mqtt_topic2, sysCfg.mqtt_subtopic, sysCfg.power, sysCfg.timezone);
      }
    }
    else if (!grpflg && !strcmp(type,"UPGRADE")) {
      if ((data_len > 0) && (payload == 1)) {
        otaflag = 3;
        sprintf_P(svalue, PSTR("Upgrade %s"), Version);
      }
      else
        sprintf_P(svalue, PSTR("1 to upgrade"));
    }
    else if (!grpflg && !strcmp(type,"OTAURL")) {
      if ((data_len > 0) && (data_len < 80))
        strcpy(sysCfg.otaUrl, (payload == 1) ? OTA_URL : dataBuf);
      sprintf_P(svalue, PSTR("%s"), sysCfg.otaUrl);
    }
    else if (!strcmp(type,"SERIALLOG")) {
      if ((data_len > 0) && (payload >= LOG_LEVEL_NONE) && (payload <= LOG_LEVEL_DEBUG_MORE)) {
        sysCfg.seriallog_level = payload;
      }
      sprintf_P(svalue, PSTR("%d"), sysCfg.seriallog_level);
    }
    else if (!strcmp(type,"SYSLOG")) {
      if ((data_len > 0) && (payload >= LOG_LEVEL_NONE) && (payload <= LOG_LEVEL_DEBUG_MORE)) {
        sysCfg.syslog_level = payload;
      }
      sprintf_P(svalue, PSTR("%d"), sysCfg.syslog_level);
    }
    else if (!strcmp(type,"LOGHOST")) {
      if ((data_len > 0) && (data_len < 32)) {
        strcpy(sysCfg.syslog_host, (payload == 1) ? SYS_LOG_HOST : dataBuf);
        restartflag = 2;
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.syslog_host);
    }
    else if (!grpflg && !strcmp(type,"SSID")) {
      if ((data_len > 0) && (data_len < 32)) {
        strcpy(sysCfg.sta_ssid, (payload == 1) ? STA_SSID : dataBuf);
        restartflag = 2;
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.sta_ssid);
    }
    else if (!grpflg && !strcmp(type,"PASSWORD")) {
      if ((data_len > 0) && (data_len < 64)) {
        strcpy(sysCfg.sta_pwd, (payload == 1) ? STA_PASS : dataBuf);
        restartflag = 2;
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.sta_pwd);
    }
    else if (!grpflg && !strcmp(type,"MQTTHOST")) {
      if ((data_len > 0) && (data_len < 32)) {
        strcpy(sysCfg.mqtt_host, (payload == 1) ? MQTT_HOST : dataBuf);
        restartflag = 2;
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.mqtt_host);
    }
    else if (!strcmp(type,"GROUPTOPIC")) {
      if ((data_len > 0) && (data_len < 32)) {
        for(i = 0; i <= data_len; i++)
          if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_';
        sprintf_P(svalue, PSTR(MQTT_CLIENT_ID), ESP.getChipId());
        if (!strcmp(dataBuf, svalue)) payload = 1;
        strcpy(sysCfg.mqtt_grptopic, (payload == 1) ? MQTT_GRPTOPIC : dataBuf);
        restartflag = 2;
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.mqtt_grptopic);
    }
    else if (!grpflg && !strcmp(type,"TOPIC")) {
      if ((data_len > 0) && (data_len < 32)) {
        for(i = 0; i <= data_len; i++)
          if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_';
        sprintf_P(svalue, PSTR(MQTT_CLIENT_ID), ESP.getChipId());
        if (!strcmp(dataBuf, svalue)) payload = 1;
        strcpy(sysCfg.mqtt_topic, (payload == 1) ? MQTT_TOPIC : dataBuf);
        restartflag = 2;
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.mqtt_topic);
    }
    else if (!grpflg && !strcmp(type,"BUTTONTOPIC")) {
      if ((data_len > 0) && (data_len < 32)) {
        for(i = 0; i <= data_len; i++)
          if ((dataBuf[i] == '/') || (dataBuf[i] == '+') || (dataBuf[i] == '#')) dataBuf[i] = '_';
        sprintf_P(svalue, PSTR(MQTT_CLIENT_ID), ESP.getChipId());
        if (!strcmp(dataBuf, svalue)) payload = 1;
        strcpy(sysCfg.mqtt_topic2, (payload == 1) ? MQTT_TOPIC : dataBuf);
      }
      sprintf_P(svalue, PSTR("%s"), sysCfg.mqtt_topic2);
    }
    else if (!grpflg && !strcmp(type,"SMARTCONFIG")) {
      if ((data_len > 0) && (payload == 1)) {
        blinks = 1999;
        smartconfigflag = 1;
        sprintf_P(svalue, PSTR("Smartconfig started"));
      } else
        sprintf_P(svalue, PSTR("1 to start smartconfig"));
    }
    else if (!grpflg && !strcmp(type,"RESTART")) {
      if ((data_len > 0) && (payload == 1)) {
        restartflag = 2;
        sprintf_P(svalue, PSTR("Restarting"));
      } else
        sprintf_P(svalue, PSTR("1 to restart"));
    }
    else if (!grpflg && !strcmp(type,"RESET")) {
      switch (payload) {
      case 1: 
        restartflag = 11;
        sprintf_P(svalue, PSTR("Reset and Restarting"));
        break;
      case 2:
        restartflag = 12;
        sprintf_P(svalue, PSTR("Erase, Reset and Restarting"));
        break;
      default:
        sprintf_P(svalue, PSTR("1 to reset"));
      }
    }
    else if (!grpflg && !strcmp(type,"LIVOLO")) {
      sprintf(sysCfg.mqtt_subtopic, "%s", type);
      if ((data_len > 0) && (data_len < 32)) {
        sendCommand((const char*)dataBuf);
        sprintf_P(svalue, PSTR("Livolo send"));
      } else 
        sprintf_P(svalue, PSTR("Livolo data wrong"));
    }
    if (!strcmp(type,"TIMEZONE")) {
      if ((data_len > 0) && (payload >= -12) && (payload <= 12)) {
        sysCfg.timezone = payload;
        rtc_timezone(sysCfg.timezone);
      }
      sprintf_P(svalue, PSTR("%d"), sysCfg.timezone);
    }
    else if ((!strcmp(type,"LIGHT")) || (!strcmp(type,"POWER")) || (!strcmp(type,"LIVOLO"))) {
      sprintf(sysCfg.mqtt_subtopic, "%s", type);
      if ((data_len > 0) && (payload >= 0) && (payload <= 2)) {
        switch (payload) {
        case 0: // Off
        case 1: // On
          sysCfg.power = payload;
          break;
        case 2: // Toggle
          sysCfg.power ^= 1;
          break;
        }
        digitalWrite(REL_PIN, sysCfg.power);
      }
      strcpy(svalue, (sysCfg.power) ? "On" : "Off");
    }
    else {
      type = NULL;
    }
    if (type == NULL) {
      blinks = 1;
      sprintf_P(stopic, PSTR("%s/%s/SYNTAX"), PUB_PREFIX, sysCfg.mqtt_topic);
      if (!grpflg)
        strcpy_P(svalue, PSTR("Status, Upgrade, Otaurl, Restart, Reset, Smartconfig, Seriallog, Syslog, LogHost, SSId, Password, MqttHost, GroupTopic, Topic, ButtonTopic, Timezone, Light, Power, Livolo"));
      else
        strcpy_P(svalue, PSTR("Status, GroupTopic, Timezone, Light, Power"));
    }
    mqtt_publish(stopic, svalue);
  }
}

void send_button(char *cmnd)
{
  char stopic[128], svalue[128];
  char *token;

  token = strtok(cmnd, " ");
  if ((!strcmp(token,"light")) || (!strcmp(token,"power"))) strcpy(token, sysCfg.mqtt_subtopic);
  sprintf_P(stopic, PSTR("%s/%s/%s"), SUB_PREFIX, sysCfg.mqtt_topic2, token);
  token = strtok(NULL, "");
  sprintf_P(svalue, PSTR("%s"), (token == NULL) ? "" : token);
  mqtt_publish(stopic, svalue);
}

void do_cmnd(char *cmnd)
{
  char stopic[128], svalue[128];
  char *token;

  token = strtok(cmnd, " ");
  sprintf_P(stopic, PSTR("%s/%s/%s"), SUB_PREFIX, sysCfg.mqtt_topic, token);
  token = strtok(NULL, "");
  sprintf_P(svalue, PSTR("%s"), (token == NULL) ? "" : token);
  mqttDataCb(stopic, (byte*)svalue, strlen(svalue));
}

void send_power()
{
  char stopic[40], svalue[20];

  sprintf_P(stopic, PSTR("%s/%s/%s"), PUB_PREFIX, sysCfg.mqtt_topic, sysCfg.mqtt_subtopic);
  strcpy(svalue, (sysCfg.power == 0) ? "Off" : "On");
  mqtt_publish(stopic, svalue);
}

void send_updateStatus(const char* svalue)
{
  char stopic[40];
  
  sprintf_P(stopic, PSTR("%s/%s/UPGRADE"), PUB_PREFIX, sysCfg.mqtt_topic);
  mqtt_publish(stopic, svalue);
}

void every_second()
{
  char stopic[40], svalue[20];

  if (heartbeatflag) {
    heartbeatflag = 0;
    heartbeat++;
    sprintf_P(stopic, PSTR("%s/%s/HEARTBEAT"), PUB_PREFIX, sysCfg.mqtt_topic);
    sprintf_P(svalue, PSTR("%d"), heartbeat);
    mqtt_publish(stopic, svalue);
  }
}

const char commands[6][14] PROGMEM = {
  {"reset 1"},        // Hold button for more than 4 seconds
  {"light 2"},        // Press button once
  {"light 2"},        // Press button twice
  {"smartconfig 1"},  // Press button three times
  {"upgrade 1"},      // Press button four times
  {"restart 1"}};     // Press button five times

void stateloop()
{
  uint8_t button;
  char scmnd[20], log[30];
  
  timerxs = millis() + (1000 / STATES);
  state++;
  if (state == STATES) {             // Every second
    state = 0;
    every_second();
  }

  button = digitalRead(KEY_PIN);
  if ((button == PRESSED) && (lastbutton == NOT_PRESSED)) {
    multipress = (multiwindow) ? multipress +1 : 1;
    sprintf_P(log, PSTR("APP: Multipress %d"), multipress);
    addLog(LOG_LEVEL_DEBUG, log);
    blinks = 1;
    multiwindow = STATES /2;         // 1/2 second multi press window
  }
  lastbutton = button;
  if (button == NOT_PRESSED) {
    holdcount = 0;
  } else {
    holdcount++;
    if (holdcount == (STATES *4)) {  // 4 seconds button hold
      strcpy_P(scmnd, commands[0]);
      multipress = 0;
      do_cmnd(scmnd);
    }
  }
  if (multiwindow) {
    multiwindow--;
  } else {
    if ((!holdcount) && (multipress >= 1) && (multipress <= 5)) {
      strcpy_P(scmnd, commands[multipress]);
      if (strcmp(sysCfg.mqtt_topic2,"0") && (multipress == 1) && mqttClient.connected())
        send_button(scmnd);          // Execute command via MQTT using ButtonTopic to sync external clients
      else
        do_cmnd(scmnd);              // Execute command internally 
      multipress = 0;
    }
  }

  if ((blinks || restartflag || otaflag) && (!(state % ((STATES/10)*2)))) {
    if (restartflag || otaflag)
      blinkstate = 0;   // Stay lit
    else
      blinkstate ^= 1;  // Blink
    digitalWrite(LED_PIN, blinkstate);
    if (blinkstate) blinks--;
  }

  switch (state) {
  case (STATES/10)*2:
    if (otaflag) {
      otaflag--;
      if (otaflag <= 0) {
        otaflag = 255;
        ESPhttpUpdate.update(sysCfg.otaUrl);
        send_updateStatus(ESPhttpUpdate.getLastErrorString().c_str());
        restartflag = 2;
      }
    }
    break;
  case (STATES/10)*4:
    CFG_Save();
    if (restartflag) {
      if (restartflag == 11) {
        CFG_Default();
        restartflag = 2;
      }
      if (restartflag == 12) {
        CFG_Erase();
        restartflag = 1;
      }
      restartflag--;
      if (restartflag <= 0) ESP.restart();
    }
    break;
  case (STATES/10)*6:
    if (smartconfigflag) {
      smartconfigflag = 0;
      WIFI_Check(WIFI_SMARTCONFIG);
    } else {
      WIFI_Check(WIFI_STATUS);
    }
    break;
  case (STATES/10)*8:
    if ((WiFi.status() == WL_CONNECTED) && (!mqttClient.connected())) {
      if (!mqttcounter)
        mqtt_reconnect();
      else
        mqttcounter--;
    }
    break;
  }
}

#ifdef SERIAL_IO
#define INPUT_BUFFER_SIZE          128

byte SerialInByte;
int SerialInByteCounter = 0;
char serialInBuf[INPUT_BUFFER_SIZE + 2];

void serial()
{
  while (Serial.available())
  {
    yield();
    SerialInByte = Serial.read();
    if (SerialInByte > 127) // binary data...
    {
      Serial.flush();
      SerialInByteCounter = 0;
      return;
    }
    if (isprint(SerialInByte))
    {
      if (SerialInByteCounter < INPUT_BUFFER_SIZE) // add char to string if it still fits
        serialInBuf[SerialInByteCounter++] = SerialInByte;
      else
        SerialInByteCounter = 0;
    }
    if (SerialInByte == '\n')
    {
      serialInBuf[SerialInByteCounter] = 0; // serial data completed
      Serial.println(serialInBuf);
      SerialInByteCounter = 0;
      do_cmnd(serialInBuf);
    }
  }
}
#endif

void setup()
{
  char log[128];

  Serial.begin(115200);
  delay(10);
  Serial.println();

  sprintf_P(Version, PSTR("%d.%d.%d"), VERSION >> 24 & 0xff, VERSION >> 16 & 0xff, VERSION >> 8 & 0xff);
  if (VERSION & 0x1f) {
    byte idx = strlen(Version);
    Version[idx] = 96 + (VERSION & 0x1f);
    Version[idx +1] = 0;
  }
  CFG_Load();
  if (sysCfg.version != VERSION) {  // Fix version dependent changes

    sysCfg.version = VERSION;
  }

  sprintf_P(Hostname, PSTR(WIFI_HOSTNAME), ESP.getChipId(), sysCfg.mqtt_topic);
  WIFI_Connect(Hostname);

  mqttClient.setServer(sysCfg.mqtt_host, MQTT_PORT);
  mqttClient.setCallback(mqttDataCb);

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, blinkstate);

  pinMode(REL_PIN, OUTPUT);
  digitalWrite(REL_PIN, sysCfg.power);

  pinMode(KEY_PIN, INPUT_PULLUP);

  rtc_init(sysCfg.timezone);

  sprintf_P(log, PSTR("App: Project %s (Topic %s, Fallback "MQTT_CLIENT_ID", GroupTopic %s) Version %s"),
    PROJECT, sysCfg.mqtt_topic, ESP.getChipId(), sysCfg.mqtt_grptopic, Version);
  addLog(LOG_LEVEL_INFO, log);
}

void loop()
{
  if (millis() >= timersec) {
    timersec = millis() + 1000;
    rtc_second();
    if ((rtcTime.Minute == 2) && (rtcTime.Second == 30)) heartbeatflag = 1;
  }

  if (millis() >= timerxs) stateloop();

  mqttClient.loop();

#ifdef SERIAL_IO
  if (Serial.available()) serial();
#endif
  
  yield();
}


Прошиваем и публикуем запись на MQTT сервере в топик cmnd/sonoff/LIVOLO с данными

RemoteID:23801,KeyCode:8


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



Заключение

Устройство у Itead получилось интересное и достаточно простое в использовании, к тому же в сообществах развиваются различные доработки и альтернативные прошивки.

Ссылки

Официальная WIKI ITEAD
Альтернативная прошивка Sonoff-MQTT-OTA-Arduino
Библиотека Livolo для Arduino
Подключение ESP8266 к MQTT серверу Mosquitto
Рubsubclient
Модернизация sonoff

Кошка по традиции



Товар предоставлен для написания обзора магазином. Обзор опубликован в соответствии с п.18 Правил сайта.
Планирую купить +85 Добавить в избранное +35 +73
+
avatar
+1
Спасибо! Я этого ждал. Очень интересные (прежде всего ценой) устройства, после обзоров жду одно, но «облако» напрягало, думал «ковырять» сам, а тут такой задел!..
+
avatar
  • chobo
  • 16 июня 2016, 16:48
+5
Здорово конечно. Процесс описан, но не очень виден результат. Не плохо бы возможности такой прошивки описать, дать пару примеров применения.
+
avatar
  • mcshel
  • 16 июня 2016, 17:24
+2
Да согласен, лучше бы еще и видео снять как это работает.
+
avatar
0
Может туплю на ночь глядя, но в итоге всех переделок вы получили возможность управлять этой штукой через выключатели Livolo? Всегда думал, что в Livolo только радио приемник стоит. И почему бы сразу не взять Sonoff RF? Разница в цене минимальная, зато ничего паять не надо.
+
avatar
  • mcshel
  • 17 июня 2016, 06:21
+1
В итоге получается, Sonoff с RF-передатчиком.

Посылаем команду -> MQTT-сервер < — Sonoff по Wifi её считывает и отправляет по RF-> выключателю Livolo

Sonoff RF имеет на борту только RF-приемник, т.е. из коробки ничего передавать не может.
+
avatar
0
Ага, понял. То есть по сути упраляение выключателями с помощью другого выключателя :) А можно узнать, как это применять? Если честь, совсем в голову не приходит, как это использовать можно.
+
avatar
  • mcshel
  • 17 июня 2016, 07:21
0
Через минут 5-10 загружу видео, думаю станет немного понятнее.
+
avatar
  • mcshel
  • 17 июня 2016, 07:37
0
Загрузил видео, только имейте ввиду, что еще стоит Wifi-роутер с MQTT сервером, через который и происходит связь, между sonoff и устройствами управления (Компьютер или/и сотовый телефон)
+
avatar
+1
Традиции со временем забываются-искажаются. Животное должно быть с обозреваемым товаром.
+
avatar
  • porno
  • 16 июня 2016, 17:12
+4
устройство внутри — включает с пульта зелёный светодиод в глазу
+
avatar
  • Omega
  • 16 июня 2016, 17:24
+1
Просветите: а родная прошивка помнит состояние контактов после пропадания (и последующего появления) питания? И параллельный вопрос про обновленную прошивку.
То, что РЕАЛЬНЫЙ статус в телефоне показывается цветом иконки это конечно радует, но не более. Не всегда можно постоянно смотреть статус, хотелось бы быть уверенным, что если включил (и электричество через минуту перемигнет), то устройство вернется в прежнее состояние. Вот например розетка Сяоми (по крайней мере 2я версия) помнит состояние.
+
avatar
  • mcshel
  • 16 июня 2016, 17:27
0
Насчет родной не уверен, альтернативная по идеи должна запоминать. Завтра еще точно проведу тест и отпишусь сюда.
+
avatar
  • mcshel
  • 17 июня 2016, 07:21
+2
Проверил, альтернативная прекрасно все запоминает.
+
avatar
+2
Прям не муська а Geektimes какой-то) Отличная статья! Спасибо!!!
+
avatar
+2
Если уж и заморачиваться альтернативной прошивкой, то лучше сразу прошить прошивку от wifi-iot.com и решить сразу все проблемы. Ее можно нормально по OTA обновлять.
+
avatar
  • mcshel
  • 16 июня 2016, 18:24
0
Эта прошивка так же поддерживает OTA. wifi-iot.com вроде же платная и закрытая? Или я ошибаюсь.
+
avatar
+2
Цена PRO версии 100 рублей. Базовая версия бесплатна. Но она стоит своих денег однозначно.
+
avatar
  • SPB-SL
  • 17 июня 2016, 19:36
0
Просто залить прошивку, без разборки и пайки возможно?
+
avatar
0
Или ESP Easy, которая тоже многое умеет, включая OTA и при этом бесплатна и открыта.
+
avatar
0
Кошка, в конце повествования, хитро высунула язык:- «Ничего не понял чувак, да?» )))
P.S. Если кому интересно, доставка одного девайса до Новосибирска 4,61 бакса показывает. Четыре девайса 8,61$ доставка.
+
avatar
0
Спасибо, будет инфа на будущее! :)
+
avatar
  • aver
  • 17 июня 2016, 19:56
0
Спасибо, отличный обзор! Тоже недавно прикупил sonoff, хотел пробовать альтернативную прошивку, детального описания не нашёл, думал, сам буду колупать, а тут такой подарок!
+
avatar
  • Coffein
  • 08 января 2017, 05:09
0
Не смог собрать скетч с livolo… Сыпет ошибками…
+
avatar
  • mcshel
  • 18 января 2017, 14:08
0
Нужно чтобы были открыты все файлы с кодом. Ошибки какие конкретно?
+
avatar
  • 086dx66
  • 16 января 2017, 11:47
0
А если включить свет кнопкой с ливоло — в итоге сервер не узнает новое состояние
А запросить состояние у ливоло невозможно
+
avatar
  • mcshel
  • 18 января 2017, 14:09
0
Да все верно пока запросить состояние у Livolo не представляется возможным.
+
avatar
  • mishava
  • 18 января 2017, 12:17
0
Скажите, по поводу связи с роутером. Если отлючат свет, в итоге роутер перезагрузится, не потеряется ли связь моего смартфона с sonoff и прийдется их заново сопряжать.
+
avatar
  • mcshel
  • 18 января 2017, 14:41
0
Зависит от прошивки, точно уже не помню как было у автора, но вполне можно проверять существует ли соединение и переподключаться. Скорее всего из коробки, такого нет и нужно программировать самостоятельно.
+
avatar
  • DRON2402
  • 20 февраля 2017, 21:22
0
Спасибо за обзор! Очень познавательно и полезно!
Подскажите, а реально реализовать управление димером от Livolo?
+
avatar
  • mcshel
  • 21 февраля 2017, 06:06
0
На странице с прошивкой есть список поддерживаемых устройств.
github.com/arendst/Sonoff-MQTT-OTA-Arduino
+
avatar
  • DRON2402
  • 26 февраля 2017, 20:29
0
На странице с прошивкой есть список поддерживаемых устройств Sonoff.
Но ни слова про поддержку димера от Livolo.
+
avatar
  • Bangkok
  • 11 сентября 2018, 17:21
0
Отличный обзор спасибо автору и участникам дискуссии.
Подскажите какая батарейка идет в пульт Sonoff?
+
avatar
  • Bangkok
  • 22 сентября 2018, 13:47
0
Добрый день. Интересный отчет.
А вопрос следующий.
Sonoff basic или RF например из коробки. Вот скажем задан цикличный таймер 20 минут включено/ 20 минут выключено.
А что будет, если интернет отрубится? Sonoff продолжит включать/выключать? Или нафиг все работать перестанет сразу, и просто будет состояние выключено?
Спасибо
+
avatar
  • spc
  • 16 июня 2019, 15:52
0
Вот передатчик в Sonoff — это круто. А в свое время я обзор, получается, пропустил.
+
avatar
0
а может кто подсказать как мне просто esp-шку прошить наоборот в sonoff? надо миниатюрную плату (по сути голый модель esp запихать в устройство) а не вот этот вот огромный sonoff с его блоком питания на 220 вольт… но сохранить поддержку работы в ewelink (голосом через алису)