Integrazione SONOFF con MQTT: esempi di programmazione

Appunti di uso sulla programmazione dei dispositivi Itead SONOFF

Queste note hanno fine esemplificativo. Esiste un software ominicomprensivo per l'uso dei dispositivi SONOFF con MTT al link seguente:

L'uso di questo software e' descritto qui:

Configurazione ambiente di programmazione

Per utilizzare il programma proposto qui bisogna configurare l'ambiente di programmazione Arduino nel modo seguente.

In preferences aggiungere

http://arduino.esp8266.com/stable/package_esp8266com_index.json

Selezionare nelle board:

ESP8266 Module

Generic ESP8285 Module

MQTT server setup

GLi esempi proposti presuppongono che un server MQTT sia raggiungibile nel broadcast domain associato allo SSID su cui i dispositivi SONOFF sono collegati. Su Linux ed altre piattaforme Unix, e' disponibile una implementazione MQTT chiamata mosquitto che puo' essere installata su Debian e derivate con il comando:

sudo apt-get install mosquitto

Per i test conviene fermare temporaneamente il demone e lanciare il server in modalita' debug:

mosquitto -v

Inoltre il servizio mDNS deve essere annunciato su mDNS in modo che tutti i nodi SONOFF possano conoscere in modo trasparente l'indirizzo del server MQTT.

avahi-publish -s "MTT server for SONOFF" _mqtt._tcp 1883 ""

Mediante il comando avahi-browse puo' essere verificato l'annuncio:

$ avahi-browse -D -a
+  wlan0 IPv4 3.11.0.73-EDO-PC.0fc7956a-5bc9-4501-b416-4b7e85e45693 _nvstream_dbd._tcp   local
+  wlan0 IPv4 DIR3500MCBT-88                                _spotify-connect._tcp local
+  wlan0 IPv4 Chromecast-4fdfe085fbdbce0ec719382e49bdc7f8   _googlecast._tcp     local
+  wlan0 IPv4 netusg20 [00:04:25:aa:03:17]                  Workstation          local
+  wlan0 IPv4 andrewp [5c:c5:d4:6b:66:74]                   Workstation          local
+  wlan0 IPv4 MQTT server for SONOFF                        _mqtt._tcp           local

Per comandare i rele occore effettuare un publish:

mosquitto_pub -h localhost -t 'sonoff/60:01:94:89:08:F0/out' -m 'K3 ON'

Idem per disattivarlo:

mosquitto_pub -h localhost -t 'sonoff/60:01:94:89:08:F0/out' -m 'K3 OFF'

La radice per inviare comandi di output ai dispositivi SONOFF e'

sonoff/<mac address>/out

Il MAC della scheda, da usare nei comandi publish, viene visualizzato nella console.

Miglioramenti

Miglioramenti possibili:

  1. pubblicare in sonoff/devices il MAC address in modo da rendere autoconfigurante il sistema per il supervisore locale

  2. pubblicare in sonoff/devices//tipo la tipologia del dispositivo (CH4, TH10/16 etc)

  3. usare mDNS per risolvere il nome del del server MQTT presente in rete (per es: mqtt.local) in modo da rendere autoconfigurante il sistema per i clients. (fatto vedi secondo esempio).

  4. usare il modulo WifiManager per rendere autoconfigurante il dispositivo nei riguardi della infrastruttura 802.11 (da verificare se il modulo PSF-A85 ha una interfaccia wireless capace della modalita' AP) secondo il data sheet (https://www.itead.cc/wiki/Sonoff_4CH) ne e' capace: network mode station/softAP/SoftAP+station

per esempio OpenHAB puo' ricevere tutti i messaggi sotto devices per comprendere quali dispositivi sono attivi in rete.

Esempio di programma per dispositivo CH4

/*
 * SONOFF CH4 2.0 relay module
 *  
 * Schematic: 
 * https://www.itead.cc/wiki/images/d/d3/Sonoff_4CH.SCHMATIC.pdf
 * 
 * Info on PSF-A85 Wifi module (as used in ITEAD SONOFF CH4): 
 * https://www.itead.cc/wiki/PSF-A85
 */ 
#include <ESP8266WiFi.h>          // https://github.com/esp8266/Arduino
#include <DNSServer.h>            // not used here, try to remove
#include <ESP8266WebServer.h>     // not used here, try to remove
#include <WiFiManager.h>          // https://github.com/tzapu/WiFiManager
#include <PubSubClient.h>         // MQTT implementation, see https://github.com/Imroy/pubsubclient

// GPIO for CH4 2.0 relay   
const int K1 = 12;

const int K2 = 5;

const int K3 = 4;

const int K4 = 15;


char ssid[] = "TanzoLab";
char password[] = "tanzolab";
IPAddress mqtt_server(192, 168, 1, 140);  // test 
//IPAddress mqtt_server(192, 168, 1, 2);  // CM£3-HOME address

// mosquitto_pub -h localhost -t 'sonoff/60:01:94:89:08:F0/out' -m 'K3 ON'


WiFiClient wifi_client;


String mac = WiFi.macAddress();   // MAC of our interface
String publish_path   = "sonoff/"+mac+"/in";
String subscribe_path = "sonoff/"+mac+"/out";



PubSubClient client(wifi_client, mqtt_server);




//gets called when WiFiManager enters configuration mode
void configModeCallback (WiFiManager *myWiFiManager) {
  Serial.println("Entered config mode");
  Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  Serial.println(myWiFiManager->getConfigPortalSSID());
  //entered config mode, make led toggle faster ?

}




void callback(const MQTT::Publish& pub) {
  int n = 0;
  // handle message arrived
  Serial.print(pub.topic());
  Serial.print(" => [");
  Serial.print(pub.payload_string());
  Serial.println("]");


  char xmac [32];
  int nk;
  char op[32];

  //if (sscanf (pub.topic().c_str(), "esp8266/%s/out/K%d %s", xmac, &nk, op ) == 3) {
  {
    const char *p;
    char buf [32];
    strcpy (buf, pub.payload_string().c_str());

    if (p = strstr(buf, "K")) {


       Serial.print("XXXXX: ");
       Serial.print(p);
       Serial.println("");

       nk = p[1] - '0';

       Serial.print("NNNNN: ");
       Serial.print(nk);
       Serial.println("");

       p += 3;

       Serial.println("COMMAND: ");
       Serial.print(p);
       Serial.println(".");

       if (!strcmp(p, "ON")) {
        Serial.print("COMMAND: ON on K");
        Serial.print(nk);
        Serial.println(".");
       switch (nk) {
         case 1:
          digitalWrite(K1, HIGH); 
         break;
         case 2:
          digitalWrite(K2, HIGH); 
         break;
         case 3: 
          digitalWrite(K3, HIGH); 
         break;
         case 4:
           digitalWrite(K4, HIGH); 
         break;  
        }
       }
       if (!strcmp(p, "OFF")) {
        Serial.print("COMMAND: OFF on K");
        Serial.print(nk);
        Serial.println(".");
       switch (nk) {
         case 1:
          digitalWrite(K1, LOW); 
         break;
         case 2:
          digitalWrite(K2, LOW); 
         break;
         case 3: 
          digitalWrite(K3, LOW); 
         break;
         case 4:
           digitalWrite(K4, LOW); 
         break;  
        }
      }
    }
  }

  #if 0

  //if (sscanf (pub.topic().c_str(), "esp8266/%s/out", xmac) == 1 && (!strcmp(xmac,mac.c_str())))
  {

    if (!strcmp (pub.payload_string().c_str(), "ledon")) {
        //digitalWrite(pinLed, LOW);    // turn the LED on (LOW is the voltage level)
        //ledStatus = true;    
    }

    if (!strcmp (pub.payload_string().c_str(), "ledoff")) {
        //ledStatus = false;
        //digitalWrite(pinLed, HIGH);    // turn the LED off (LOW is the voltage level)
    }

  }
  #endif
}


void setup() {
  static int i = 0;
  Serial.begin(115200);
  delay(10);

  // We start by connecting to a WiFi network
  // station mode
  WiFi.mode(WIFI_STA);

  if (strlen(password))
    WiFi.begin(ssid,password);
  else
    WiFi.begin(ssid);


  Serial.println();
  Serial.println("Connecting to WiFi");

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


    Serial.print(i);
    Serial.print(": connecting to ");
    Serial.println(ssid);

    Serial.printf("Connection status: %d\n", WiFi.status());
    Serial.println("--------");
    WiFi.printDiag(Serial);
    Serial.printf("\n........\n\n");
    ++i;

  }

  client.set_callback(callback);

  Serial.println();
  Serial.print("WiFi connected with ip ");  
  Serial.println(WiFi.localIP());

  Serial.print("MAC:");
  Serial.println(mac);

  pinMode(K1, OUTPUT);
  pinMode(K2, OUTPUT);
  pinMode(K3, OUTPUT);
  pinMode(K4, OUTPUT);
}


// the loop function runs over and over again forever
void loop() {
  static int i = 0;

  if (WiFi.status() == WL_CONNECTED) {
    if (!client.connected()) {
      Serial.println("connecting to MQTT....");
      if (client.connect("arduinoClient")) {
        Serial.println("MQTT connected");
        client.publish(publish_path,"START");
        client.subscribe(subscribe_path);
      }
    }  
    if (client.connected()) client.loop();
  }

  delay(1000);
  Serial.println("Loop");

  char msg[128];
  sprintf (msg, "Loop: %d", i++);
  client.publish("esp8266", msg);
}

Esempio per dispositivo TH10/16 (relay singolo 16 A)

/*
 * SONOFF TH10/16 MQTT management 
 * 
 * Andrea Montefusco
 */

#include <ESP8266WiFi.h>          // https://github.com/esp8266/Arduino
#include <ESP8266mDNS.h>
#include <WiFiManager.h>          // https://github.com/tzapu/WiFiManager
#include <PubSubClient.h>



static const uint8_t RELAY = 12; // MTDI
static const uint8_t SWITCH = 0; // GPIO0
static const uint8_t LED = 13; // MTCK



class BlinkingStatusLed {
  private:

    unsigned long time_to_blink;
    uint8_t pin;
    long period; // in ms
    float duty_cycle;

    static const uint8_t LED_STATE_OFF          = 0;
    static const uint8_t LED_STATE_BLINKING_ON  = 10;
    static const uint8_t LED_STATE_BLINKING_OFF = 20;
    static const uint8_t LED_STATE_ON           = 30;

    uint8_t state = LED_STATE_OFF;

public:

    BlinkingStatusLed (uint8_t pin_ = 0, long period_ = 1000, float duty_cycle_ = 0.3): 
      pin(pin_), period(period_), duty_cycle(duty_cycle_)
    {
      pinMode(pin, OUTPUT); 
      digitalWrite(pin, HIGH);    // turn the LED off (LOW is the voltage level)
      state = LED_STATE_OFF;
    }

    void run ()
    {
      if (millis() < time_to_blink) return;
      switch (state) {
      case LED_STATE_BLINKING_ON:
          digitalWrite(pin, LOW);  // change status to On
          time_to_blink = millis() + (long)((float)period*duty_cycle);
          state = LED_STATE_BLINKING_OFF;
          //Serial.print("ON ");
          //Serial.println((float)period*duty_cycle);
          break;
      case LED_STATE_BLINKING_OFF:
          digitalWrite(pin, HIGH);  // change status to OFF
          time_to_blink = millis() + (long)((float)period*(1-duty_cycle));
          state = LED_STATE_BLINKING_ON;
          //Serial.print("OFF ");
          //Serial.println(((float)period*(1-duty_cycle)));
      }
    }

    void on() {
      state = LED_STATE_ON;
      digitalWrite(pin, LOW);
    }

    void off() {
      state = LED_STATE_ON;
      digitalWrite(pin, HIGH);      
    }

    void blink (long period_, float duty_cycle_) {
      period = period_;
      duty_cycle = duty_cycle_;
      blink();
    }

    void blink () {
      time_to_blink = millis();
      state = LED_STATE_BLINKING_ON;
    }
};

BlinkingStatusLed sys_led(LED);

/*
 * Relay class
 * 
 * from https://github.com/ImUrlaub/Sonoff
 * 
 * manages the single relay in TH10/16 device
 * the relay is drive through the ESP8265 PWM_0 output.
 * the idea is to provide the full output available when the relay 
 * is turned on, keeping the max power just for the amount of time
 * needed to move the contact in close position. 
 * Afterwards the power is decreased (changing the PWM duty cycle) 
 * in order to save power.
 * 
 */

class Relay
{
  private:

    unsigned long time_to_reduce_current;
    unsigned long millis_till_reduction;
    uint16_t reduced_current;

    uint8_t pin;

    static const uint8_t RELAY_STATE_OFF = 0;
    static const uint8_t RELAY_STATE_ON_WAIT_TO_REDUCE = 1;
    static const uint8_t RELAY_STATE_ON_REDUCED = 1;
    uint8_t state = RELAY_STATE_OFF;

  public:

    Relay(uint8_t pin = 0, float reduction = 0.4, unsigned long millis_till_reduction = 500, unsigned long analog_write_frequency = 10000) {
      setReduction(reduction);

      setMillisTillReduction(millis_till_reduction);

      analogWriteFreq(analog_write_frequency);

      this->pin = pin;
      pinMode ( pin, OUTPUT );
    }

    void on() {
      switch (state) {
        case RELAY_STATE_OFF:
          analogWrite(pin, PWMRANGE);
          time_to_reduce_current = millis() + millis_till_reduction;
          state = RELAY_STATE_ON_WAIT_TO_REDUCE;
          break;
      }
    }

    void off() {
      analogWrite(pin, 0);
      state = RELAY_STATE_OFF;
    }

    bool isOn() {
      return state != RELAY_STATE_OFF;
    }

    void setReduction(float reduction) {
      if ( reduction > 1.0 ) reduction = 1.0;
      else if ( reduction < 0.0 ) reduction = 0.0;
      reduced_current = (PWMRANGE * 1.0) * reduction;
    }

    void setMillisTillReduction(unsigned long millis_till_reduction) {
       this->millis_till_reduction = millis_till_reduction;
    }

    void loop() {
      switch (state) {
        case RELAY_STATE_ON_WAIT_TO_REDUCE:
          if (millis() > time_to_reduce_current) {
            analogWrite(pin, reduced_current);
            state = RELAY_STATE_ON_REDUCED;
          }
          break;
      }
    }

};


Relay relay (RELAY);



//char ssid[] = "TanzoLab";
//char password[] = "tanzolab";

const char ssid[] = "Telecom-30203937";
const char password[] = "dSHxm8uVVPhCnSlyjV6Db5Vs";

IPAddress mqtt_server(192, 168, 0, 30);
//IPAddress mqtt_server(192, 168, 1, 140);  // test 
//IPAddress mqtt_server(192, 168, 1, 2);  // CM£3-HOME address

// mosquitto_pub -h localhost -t 'sonoff/60:01:94:89:08:F0/out' -m 'K3 ON'


WiFiClient wifi_client;


String mac = WiFi.macAddress();   // MAC of our interface
String publish_path   = "sonoff/"+mac+"/in";
String subscribe_path = "sonoff/"+mac+"/out";

PubSubClient mqtt_client(wifi_client, mqtt_server);

//
// called when something is published
//
void callback(const MQTT::Publish& pub) {
  int n = 0;

  // handle message arrived
  Serial.print(pub.topic());
  Serial.print(" => [");
  Serial.print(pub.payload_string());
  Serial.println("]");

  const char *p;
  char buf [32];
  int nk;

  strcpy (buf, pub.payload_string().c_str());

  if (p = strstr(buf, "K")) {
       nk = p[1] - '0';
       p += 3;  // skip to command

       if (!strcmp(p, "ON")) {
         if (nk == 1) relay.on();
       }
       if (!strcmp(p, "OFF")) {
         if (nk == 1) relay.off();
       }
       if (strstr(p, "BLINK")) {
          p += 6;
          if (!strcmp(p, "ON")) sys_led.blink();

          if (!strcmp(p, "OFF")) sys_led.off();
       }
  }
}


void setup() {
  static int i = 0;
  Serial.begin(115200);
  delay(10);

  // We start by connecting to a WiFi network
  // station mode
  WiFi.mode(WIFI_STA);

  if (strlen(password))
    WiFi.begin(ssid,password);
  else
    WiFi.begin(ssid);


  Serial.println();
  Serial.println("Connecting to WiFi");

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


    Serial.print(i);
    Serial.print(": connecting to ");
    Serial.println(ssid);

    Serial.printf("Connection status: %d\n", WiFi.status());
    Serial.println("--------");
    WiFi.printDiag(Serial);
    Serial.printf("\n........\n\n");
    ++i;

  }

  mqtt_client.set_callback(callback);

  Serial.println();
  Serial.print("WiFi connected with ip ");  
  Serial.println(WiFi.localIP());

  Serial.print("MAC:");
  Serial.println(mac);


  ///////////////////////////////////////////////////////////
  //
  // mDNS client and responder
  //
  ///////////////////////////////////////////////////////////
  if (!MDNS.begin(("sonoff-"+mac).c_str())) {
    Serial.println("Error setting up MDNS responder!");
  }
  Serial.println("mDNS responder started");
  MDNS.addService("sonoff-"+mac, "tcp", 8080); // Announce esp tcp service on port 8080

  // activate led
  sys_led.blink(500, 0.1);
}



//gets called when WiFiManager enters configuration mode
void configModeCallback (WiFiManager *myWiFiManager) {
  Serial.println("Entered config mode");
  Serial.println(WiFi.softAPIP());
  //if you used auto generated SSID, print it
  Serial.println(myWiFiManager->getConfigPortalSSID());
  //entered config mode, make led toggle faster ?

}




bool mqtt_server_found = false;



// the loop function runs over and over again forever
void loop() {


  // resolve the MQTT server name
  static int mdns_f = 0;
  if (mdns_f == 0) {

    Serial.println("Sending mDNS query");

    // _googlecast._tcp

    int n = MDNS.queryService("mqtt", "tcp"); // Send out query for esp tcp services
    Serial.println("mDNS query done");
    if (n == 0) {
       Serial.println("no services found");
       sys_led.blink(1000, 0.2);
    } else {
       Serial.print(n);
       Serial.println(" service(s) found");
       for (int i = 0; i < n; ++i) {
          // Print details for each service found
          Serial.print(i + 1);
          Serial.print(": ");
          Serial.print(MDNS.hostname(i));
          Serial.print(" (");
          Serial.print(MDNS.IP(i));
          Serial.print(":");
          Serial.print(MDNS.port(i));
          Serial.println(")");
          mqtt_server = MDNS.IP(i);
          mqtt_server_found = true;
          mdns_f = 1;
       }
       sys_led.blink(1000, 0.15);
    }
    Serial.println();
  }

  //
  // MQTT processing
  //
  static int i = 0;

  if (WiFi.status() == WL_CONNECTED && mqtt_server_found == true) {

    if (!mqtt_client.connected()) {
      Serial.println("connecting to MQTT local server....");
      if (mqtt_client.connect("SONOFF")) {
        Serial.println("MQTT connected");
        mqtt_client.publish(publish_path,"START");
        mqtt_client.subscribe(subscribe_path);
      }
    }
    if (mqtt_client.connected()) mqtt_client.loop();
  }

  if (0) {
     delay(1000);

     char msg[128];
     sprintf (msg, "Loop: %d", i++);
     mqtt_client.publish("esp8266", msg);
     Serial.println(msg);
  }

  // run relay class instance
  relay.loop();

  // led
  sys_led.run();

}

output del programma

4: connecting to Telecom-xxxx
Connection status: 3

Mode: STA
PHY mode: N
Channel: 11
AP id: 0
Status: 5
Auto connect: 1
SSID (16): Telecom-xxxx
Passphrase (24): xxxxxxxxxxx
BSSID set: 0

WiFi connected with ip 192.168.0.39
MAC:5C:CF:7F:41:E2:5A
mDNS responder started
Sending mDNS query
mDNS query done
1 service(s) found
1: andrewp (192.168.0.30:5900)

connecting to MQTT local server....
MQTT connected
Loop: 0
Loop: 1

Link al sito ITEAD

Sergio Tanzilli (Autore - Progettista hw & sw - Imprenditore - Musicista dilettante, classe 1964)
Fondatore di Area SX srl, Acme Systems srl e del TanzoLab, si occupa dal 1974 di elettronica digitale, dal 1978 di microprocessori, dal 1992 di microcontroller, dal 1995 di Linux e pubblicazioni su Web, dal 2005 di progettazione, produzione e commercializzazione di schede Linux Embedded.
http://www.tanzilli.com - http://www.acmesystems.it - https://github.com/tanzilli - sergio@tanzilli.com

Andrea Montefusco
Currently employed as network architect, always Internet working man, real C/C++ programmer in the past, network and Unix system engineer as needed, HAM Radio enthusiast (former IW0RDI, now IW0HDV), aeromodeller (a person who builds and flies model airplanes) since 1976 (ex FAI10655).
http://www.montefusco.com - https://github.com/amontefusco - https://github.com/IW0HDV - andrew@montefusco.com



Le attività del TanzoLab si svolgono ogni mercoledi sera, salvo casi speciali, dalle ore 18:30 presso i locali della Acme Systems srl e consistono in:

  • Talk monotematici a cura di professionisti in vari settori tecnologici
  • Workshop pratici su elettronica embedded, produzione e informatica
  • Progettazione e realizzazione di nuovi prodotti embedded per l'IT

Le attività vengono coordinate tramite questo sito, in cui vengono pubblicati tutti i lavori svolti o in via di sviluppo, e tramite un gruppo Telegram con cui per interagire direttamente via chat con gli altri membri.