WiFi-enabled AC Light Switch

WiFi-enabled AC Light Switch

A light switch that can be incorporated into a 2-way household switching circuit to add WiFi control while leaving the existing switching working in a way that requires the user to have no knowledge of the WiFi switch and allows installation using wiring conventions that are recognisable to anyone familiar with traditional 2-way switches.

Troubleshooting of the lighting and switching installation is then possible without an in-depth understanding of the WiFi switch and removal, to restore the lighting and switching installation to a “non-smart” state is simple and achievable by someone with no understanding of the WiFi switch.

The switch (project _099) is a PCB-based relay device with screw terminals to integrate mains wiring and an ESP-01 (ESP8266-based) microcontroller and requiring a separate 3.3v DC power supply which, in my case, is provided by a whole-house DC power supply (see separate project here). The PCB-based device is mounted on a board which eases installation by providing screw terminals for traditional wall switches elsewhere, a lamp output terminal, and a junction box for bringing in the lighting ring supply without having to disturb too much of the existing house wiring. It also allows for easy bench testing before installation in, often confined and difficult to access, loft areas.

This example is a switch installed in conjunction with a 2-way wall switch and a wall-mounted flex outlet supply a switched live and neutral to an LED driver for over-wardrobe LED downlighters. Others use a 2-way wall switch, intermediate wall switch and a WiFi-enabled switch to switch room lights or ceiling-hanging bedside lamps.

In the images below the first 2-way switch should be replaced with a WiFi switch wired in exactly the same way

In the sketch below all sensitive information (WiFi SSIDs and passwords, etc) are referenced in a separate file (CBL.h and CBL-values.h) which can be created and saved in the Arduino IDE’s libraries folder and which should take the form of a standard .ino file, or be included within the code below with the #include line removed.

/*
 ***************************************************************************************************
 *      ESP8266 MQTT Single-Channel Mains Switch with AC sensing to enable use as 2-way switch     *
 ***************************************************************************************************
 *
 * This sketch subscribes to an MQTT topic and toggles GPIO2 depending on message, current state
 * of relay (NC, NO - equivalent ot L1, L2 on a manual/wall light switch), current state of AC (note
 * that _099 v2.0 board allows sensing of L1, L2, or COM depending on position of the single 680k
 * resistor - for use as a single switch or as a 2-way switch alongside manual 2-way or intermediate
 * switches) and whether or not any change is as a result of manual switch position changing, or
 * network/server software instruction (MQTT)
 * 
 * Node-RED needs to control only the state of the usual topic in the usual way - on or off - it
 * doesn’t need to know anything about the fact that the switch monitors the state of the AC output 
 * in order to decide which way to toggle the relay. That means Node-RED controls for timers, Alexa, 
 * UI, etc, can match the simple WiFi switch-enabled devices. Also, this sketch handles the publish
 * of the usual topic into MQTT and, therefore, Node-RED if the lamp state changes because of a wall
 * switch - ie the ESP software detects the change when the network software hadn't initiated anything.
 * 
 * For use as a 2-way switch in a circuit with other 2-way switches connect lamp across L2 and N with
 * 680k resistor (to opto-isolator) on L2. See drawing in projects folder.
 * 
 * UPDATE required - instructions for use as sinlge switch. Also, have tested above only with an _099, 
 * a 2-way wall switch and an intermediate wall switch. Possibly needs testing with only a single
 * 2-way switch in addition (ie no intermediate)
 * 
 * The listen topic can be changed in the variables at the top of the sketch - clearly each device
 * needs to listen on a unique topic
 * 
 * Note that GPIO2 starts high at ESP8266 startup/boot so holding it high until the sketch has fully
 * started means it doesn't flick on for half a second then off
 * 
 * WiFi & MQTT settings are in cbl-values.h
 * 
 * Changelog (DEVelopments & VERsions in development, FORKs & REVisions in production)
 * *********
 * 
 * --- ver C (29/05/2021) --- 
 * - added KeepAlive
 * 
 * --- ver B (11/05/2021) ---
 * - added debounce - ie only processes the loop every 200ms - non-blocking so as not to miss an
 * MQTT broadcast
 * 
 * --- ver A (10/05/2021) --- TESTED WORKING ON BENCH PSU, see below for issues
 * - first version - based on _010-devA_verK
 * 
 * - on a step-down DC-DC (as used from house DC power) it bounces two or three times when activated
 * from network MQTT but doesn't from a manual switch. Affects both on and off in the same way
 * Tried putting a 470uF cap across Gnd-3v3 to no avail. Does NOT happen on bench power
 * 
 * 
 * Settings unique to each new deployment
 * **************************************
 * 
 * hostname_friendly      : update for each device (eg the ESP device controlling relays for Xmas lights could be "XmasLights")
 * deploymentID           : update for each device (used to create MQTT topics)
 * 
 * 
 * Settings unique to each new environment (eg for use in another LAN)
 * *******************************************************************
 * 
 * system_messages_topic  : update for each new system - eg a new LAN with a new Node-RED environment
 * NOW TAKEN FROM CBL-VALUES.H....MQTT settings          : update for each new system or a new MQTT server within the system
 * NOW TAKEN FROM CBL-VALUES.H....WiFi settings          : update for the available SSIDs (router/WAP/extender) - list multiple SSIDs
 * 
 */

/**************** include the libraries *******************************/
#include <CBL-values.h>
#include <CBL.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include "WiFiAutoSelector.h"

/**************** define variables for use in programme ***************/
char str_ip[16];
char publish_topic[49];
boolean ac_now_state;   // present state of first sfh628 output (1 is energised)
boolean ac_prev_state;

char listen_topic[49];        // listen for switch instructions
boolean relay_state;    // present state of relay (1 is L2, energised, Normally Open; 0 is L1, Normally Closed)

boolean lamp_topic_now_state; // present state of lamp MQTT topic - ie what is the network saying the lamp is doing
boolean lamp_topic_prev_state;

long last_processed = 0; // holds a time counter for delaying processing of the trigger process in loop() without blocking MQTT callback by using delay()
long last_keepalive = 0; // holds a time counter for the last received keepalive message

/**************** assign variable values for use in programme *********/

//const char* hostname_friendly = "WardrobeLights";
//const char deployment_id[7] = "W4DR0B";

/**************** set def values **************************************/
#define sfh628_pin 0 // connect SFH628 output to pin GPIO0
#define relay_pin 2  // connect relay input to pin GPIO2
#define WIFI_CONNECT_TIMEOUT 8000

//#define debug_mode 1
#define debug_mode 0

#define debounce_interval 200 // software debounce threshold

/**************** WiFi network settings *******************************/
// take WiFi variables from CBL-values.h
// update to remove this section but put a note in the header block to detail that the values are taken from CBL-values and used in subroutines

/**************** MQTT server settings ********************************/
// take MQTT variables from CBL-values.h
// update to remove this section but put a note in the header block to detail that the values are taken from CBL-values and used in subroutines

/**************** initialise libraries ********************************/
WiFiClient espClient;                     // Initialize the WiFi client library
PubSubClient mqttClient(espClient);       // bind the MQTT client instance to the Ethernet Client instance
WiFiAutoSelector wifiAutoSelector(WIFI_CONNECT_TIMEOUT);  

void setup() {
 
  pinMode(sfh628_pin,INPUT); // for sense
  
  digitalWrite(relay_pin, HIGH); 
  pinMode(relay_pin, OUTPUT); // for switch (relay)
  digitalWrite(relay_pin, HIGH);

  Serial.begin(115200);

  //get this into CBL.h ??? Only if it can be done in a way that detects if its a ESP8266 and the same CBL.h can be used for all devices
  Serial.println(hostname_friendly);
  
  display_sketch_id(__FILE__); // display sketch file name in serial monitor so we know what is running on this device - requires CBL.h

  WiFi.hostname(hostname_friendly);
  wifi_connect(); // scan and connect to WiFi
  
  setup_OTA(hostname_friendly); // start OTA
  
  // for sense
  strcpy(publish_topic,devices_listen_topic_root);
  strcat(publish_topic,deployment_id);
  strcat(publish_topic,"-sense");
  
  // for switch
  strcpy(listen_topic,devices_listen_topic_root);
  strcat(listen_topic,deployment_id);

  /**************** MQTT bits *****************************/
  mqttClient.setServer(cbl_values_h__mqtt_server, cbl_values_h__mqtt_port);
  mqttClient.setCallback(callback);
  if (!mqttClient.connected()) {
    reconnect();
  }
  mqttClient.loop();

  /**************** log system start event ****************/
  Serial.println(listen_topic);
  Serial.println(publish_topic);
  
  mqttClient.publish(system_messages_topic, str_ip);
  mqttClient.publish(system_messages_topic, return_sketch_id(__FILE__)); // log sketch file name so we know what is running on this device - requires CBL.h 
  mqttClient.publish(system_messages_topic, hostname_friendly); // log host_name so we know which device is running
  mqttClient.publish(system_messages_topic, publish_topic);
  mqttClient.publish(system_messages_topic, listen_topic);

  ac_now_state = 0;
  ac_prev_state = 0;
  lamp_topic_now_state = 0;
  lamp_topic_prev_state = 0;

  delay(1000);
  digitalWrite(relay_pin, LOW);
  relay_state = 0;
  
}

void loop() {
  
  // monitor MQTT KeepAlive messages
  mqtt_keepalive();

  // check for and handle any WiFi disconnect
  wifi_connect();

  // if MQTT disconnected then reconnect
  if (!mqttClient.connected()) { // if MQTT disconnects
    reconnect();
  }
  mqttClient.loop(); 

  if (millis() - last_processed > debounce_interval) { // only process the loop once every 'debounce_interval'
    
    ac_now_state = digitalRead(sfh628_pin);

    if (ac_now_state != ac_prev_state) { // ie, if its changed since last loop
      if (ac_now_state) {
        mqttClient.publish(publish_topic,"1");
        if (ac_now_state != lamp_topic_now_state) { // ie if lamp is on but network says its off then that
                                                    // has to be as a result of a manual switch of a two-way 
                                                    // or intermediate switch on the wall
          mqttClient.publish(listen_topic,"1");
        }
      } else {
        mqttClient.publish(publish_topic,"0");
        if (ac_now_state != lamp_topic_now_state) { // ie if lamp is off but network says its on then that
                                                    // has to be as a result of a manual switch of a two-way 
                                                    // or intermediate switch on the wall
          mqttClient.publish(listen_topic,"0");
        }
      }
      lamp_topic_now_state = ac_now_state; // do this manually here so internal variable is accurate IMMEDIATELY - without waiting for MQTT
      lamp_topic_prev_state = lamp_topic_now_state;
      Serial.print("Current AC state : ");
      Serial.println(ac_now_state);
    }
  
    if (lamp_topic_now_state != lamp_topic_prev_state) {  // ie published logic change from the network - either
                                                          // by ESP software or by Node-RED software
      if (lamp_topic_now_state != ac_now_state) { // ie it happened because of Node-RED software
        // toggle relay
        if (relay_state) { // if its at L2 de-energise it/turn off
          digitalWrite(relay_pin, LOW);
          Serial.println("--- COM switched to L1 ---");
          relay_state = 0;
        } else { // if its at L1 energise it/turn on
          digitalWrite(relay_pin, HIGH);
          Serial.println("*** COM switched to L2 ***");
          relay_state = 1;
        }
      }
    }
  
    ac_prev_state = ac_now_state;
    lamp_topic_prev_state = lamp_topic_now_state;

    last_processed = millis();
    
  }
  
}

/**************** SUBROUTINES *****************************************/

/**************** Reconnect MQTT **************************************/
void reconnect() {
  // create a unique clientID
  char compile_timestamp[] = __DATE__ __TIME__;
  char mqtt_clientid[] = __DATE__ __TIME__;

  int j = 0;
  
  for (int i = 0; i < sizeof(compile_timestamp) - 1; i++){
    if(compile_timestamp[i] != ':' && compile_timestamp[i] != ' ') {
      mqtt_clientid[j] = compile_timestamp[i];
      j++;
    }
  }
  mqtt_clientid[j] = '\0';

  // Loop until we're reconnected
  while (!mqttClient.connected()) {
    Serial.print("Attempting MQTT connection...");
    if (mqttClient.connect(mqtt_clientid)) {
      Serial.print("MQTT connected with client ID: ");
      Serial.println(mqtt_clientid);
      // ... and subscribe to topic
      mqttClient.subscribe(listen_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(mqttClient.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

/**************** MQTT callback ***************************************/
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] : ");
  for (int i=0;i<length;i++) {
    char receivedChar = (char)payload[i];
    Serial.println(receivedChar);
  }
 
  if (strcmp(topic,keepalive_topic)==0) {
    Serial.print("KeepAlive received...resetting keepalive counter (last_keepalive)");
    last_keepalive = millis();
  }
  
  if (strcmp(topic,listen_topic)==0) {
    for (int i=0;i<length;i++) {
      Serial.print("Trigger : ");
      char receivedChar = (char)payload[i];
      if (receivedChar == '1') {
        lamp_topic_now_state = 1;
      } else {
        lamp_topic_now_state = 0;
      }
    }
  }
  
  Serial.println();
}

/**************** WiFi Connect / Re-connect ***************************/
void wifi_connect(){

  char str_ip[16];
  
  while(WiFi.status() != WL_CONNECTED) {
    
    WiFi.mode(WIFI_STA);

    // add however many SSIDs there are in cbl_values_h for attempting to connect to
    for (byte row = 0; row < 9; row++){
      wifiAutoSelector.add(cbl_values_h__ssid[row],cbl_values_h__password[row]);
    }
    
    Serial.print("\nConnecting WiFi");
    if(-1 < wifiAutoSelector.scanAndConnect()) {
      int connectedIndex = wifiAutoSelector.getConnectedIndex();
      Serial.print(".....success!\nConnected to '");
      Serial.print(wifiAutoSelector.getSSID(connectedIndex));
      Serial.println("'");
      
      Serial.print("IP Address is: ");
      Serial.println(WiFi.localIP());
      sprintf(str_ip, "IP:%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3] );
    }
  }
}

/**************** MQTT KeepAlive ***************************/
void mqtt_keepalive(){
  
  if (millis() - last_keepalive > keepalive_threshold) {
    Serial.println("MQTT quiet for longer than keepalive_threshold");
    ESP.restart();
    last_keepalive = millis();
  }
}

cbl-values.h (in <arduino_folder>/libraries/cbl-values/cbl-values.h)


/**************** MQTT topics and topic-roots *******************************/
#define system_messages_topic "XXXXXX/SystemInfo"
#define devices_listen_topic_root "XXXXXX/Infrastructure/Devices/"
#define temp_publish_topic_root "XXXXXX/Infrastructure/TempDevices/"
#define light_publish_topic_root "XXXXXX/Infrastructure/Devices/"
#define blink_topic "XXXXXX/Debug"
#define keepalive_topic "XXXXXX/KeepAlive"
//#define switches_publish_topic_root "XXXXXX/Infrastructure/Switches/"
//#define sensors_publish_topic_root "XXXXXX/Infrastructure/Sensors/"

/**************** Stuff for testing *****************************************/
#define cbl_values_h__my_delay 1000

/**************** MQTT server settings **************************************/
//const char* cbl_values_h__mqtt_server = "192.168.XX.XX";
const char* cbl_values_h__mqtt_server = "mqtt.domain.tld";
const char* cbl_values_h__testmosquitto_server = "test.mosquitto.org";
const int cbl_values_h__mqtt_port = 1883;

/**************** WiFi network settings *************************************/
const char* cbl_values_h__ssid[] = {
	"XXXXXXXX",
	"YYYYYYYY",
	"ZZZZZZZZ",
	"",
	"",
	"",
	"",
	"",
	""
}; //replace this with your WiFi network names separated by commas
// there must always be 9 entries - use empty string ("") if fewer than 9
const char* cbl_values_h__password[] = {
	"password",
	"password",
	"password",
	"",
	"",
	"",
	"",
	"",
	""
}; //replace this with your WiFi network password separated by commas
// there must always be 9 entries - use empty string ("") if fewer than 9

/**************** GPRS APN settings - Mobile Network *******************************/
const char cbl_values_h__virgin_apn[]  = "mobiapn.tld";
const char cbl_values_h__virgin_user[] = "";
const char cbl_values_h__virgin_pass[] = "";

/**************** Miscellaneous *******************************/
// KeepAlive heartbeat provided by MQTT broker to allow clients
// to reset if not connected and usual reconnect routines are not
// working. This was experienced when mosquitto was upgraded and 
// suddenly started rejecting anonymous clients. Even when 
// reconfiguring mosquitto to accept anonymous clients no devices
// would connect again until restarted. Note that, while a minute
// or less is a workable threshold that causes clients to repeatedly
// restart while routinely restarting mosquitto (eg during upgrade)
// so setting to 5 minutes avoids this while maintaining a useful
// guard against failed connections.
#define keepalive_threshold 300000

cbl.h (in <arduino_folder>/libraries/cbl/cbl.h)

#include "Arduino.h"
#include <string.h>

//#define MQTT_system_topic "XXXXXX/SystemInfo"

/* DISPLAY SKETCH DETAILS - display_sketch_id()
 *
 * Call this routine at the start of a sketch in order to display the sketch filename in a serial monitor
 * so as to identify what is running on a device when its been a while since uploading
 * 
 * Call by including this header:
 * 
 * #include "sketch_id.h" (if local)
 * or
 * #include <sketch_id.h> (if installed library)
 * 
 * then calling:
 * 
 * display_sketch_id(__FILE__);
 * 
 * in order to pass the __FILE__ property of the sketch (ie referencing __FILE__ directly from this header would display the filename of this header
 * 
 *
 * AND/OR use return_sketch_id(__FILE__);
 *
 * to pass the same string back to the calling routine so as to publish with MQTT
 */
 
void display_sketch_id(const char* file){

  char str_sketch_id[199] = "";
  
  file = strrchr(file, '/') ? strrchr(file, '/') + 1 : file;

  sprintf(str_sketch_id,"\n\nSystem Startup! Sketch running is %s (compiled at %s, on %s)\n\r", file,__TIME__,__DATE__);

  Serial.print(str_sketch_id);
  Serial.println("----------------------------------");
  Serial.println();
}

char *return_sketch_id(const char* file){

  char str_sketch_id[199] = "";
  
  file = strrchr(file, '/') ? strrchr(file, '/') + 1 : file;

  sprintf(str_sketch_id,"%s", file);

  return str_sketch_id;
}

Leave a Reply

Your email address will not be published. Required fields are marked *