Using a Sonoff S31 with Blynk

testing

This post will describe how the Itead Sonofff S31 can control a household AC electric device with a cellphone loaded with the Blynk application. Additionally, we can use the S31 to monitor energy usage by keeping track of real-time power, current and voltage of the connected appliance.

s31

A Blynk timer widget allows automatic on/off control via a time schedule. The provided source code also includes Amazon Alexa voice command compatibility.

screenshot_2019-01-21-08-08-56

The Sonoff S31 incorporates an CSE7766 single-phase, multi-function measuring chip, which provides measurements of electric current, voltage and power through the ESP8266 UART. The CSE7766 operates at a baud rate of 4800bps (± 2%), with 8-bit data, 1 even parity check and 1 stop bit. The datasheet contains all the details and can be downloaded here.

powermeter

The S31 device was constructed in such a way that taking them apart is easy. You will need to solder 4 wires or a 4-pin header to the test points on the exposed PCB in order to upload the new firmware. Instead of duplicating the instructions for disassembly and reflashing of the Sonoff S31, simply see the blog here.


/*************************************************************************
 * Title: Simple ESP-8266 Amazon Echo/sonoff wifi relay control
 * File: sonoff_echo_blynk_Utility.ino
 * Author: James Eli
 * Date: 5/1/2017
 * 
 * Sonoff S31 ESP8266 "utility".
 *   Uses Blynk App V0 (switch), V1 (indicator LED), V2 (timer), V3 (voltage), 
 *   V4 (current), V5 (power), V6 (energy)
 *
 * This program controls a Sonoff wifi relay module communicating either 
 * through an amazon echo or the Blynk android application to the Sonoff 
 * onboard esp-8266 module. Amazon Echo responds to ECHO_KEY_WORD.
 *************************************************************************/
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <WiFiManager.h> 
#include <ESP8266WebServer.h>
#include <BlynkSimpleEsp8266.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <SimpleTimer.h>
#include "WemoSwitch.h"
#include "WemoManager.h"
#include "CallbackFunction.h"

bool isFirstConnect = true;       // Flag for re-sync on connection.
int relayState = LOW;             // Blynk app pushbutton status.

// Blynk app authentication code.
char auth[] = "...";

boolean SwitchReset = true;       // Flag indicating that the hardware button has been released

// esp8266 pins.
#define ESP8266_GPIO13  13        // Sonof green LED (LOW == ON).
#define ESP8266_GPIO0   0         // Sonoff pushbutton (LOW == pressed).
#define ESP8266_GPIO12  12        // Sonoff relay (HIGH == ON).
const int RELAY = ESP8266_GPIO12; // Relay switching pin. Relay is pin 12 on the SonOff
const int LED = ESP8266_GPIO13;   // On/off indicator LED. Onboard LED is 13 on Sonoff
const int SWITCH = ESP8266_GPIO0; // Pushbutton.

// CSE7766 data.
double power = 0;
double voltage = 0;
double current = 0;
double energy = 0;
double ratioV = 1.0;
double ratioC = 1.0;
double ratioP = 1.0;
// Serial data input buffer.
unsigned char serialBuffer[24];
// Serial error flags.
int error;
// Energy reset counter.
int energyResetCounter;
#define MAX_ENREGY_RESET_COUNT 12

// CSE7766 error codes.
#define SENSOR_ERROR_OK             0       // No error.
#define SENSOR_ERROR_OUT_OF_RANGE   1       // Result out of sensor range.
#define SENSOR_ERROR_WARM_UP        2       // Sensor is warming-up.
#define SENSOR_ERROR_TIMEOUT        3       // Response from sensor timed out.
#define SENSOR_ERROR_UNKNOWN_ID     4       // Sensor did not report a known ID.
#define SENSOR_ERROR_CRC            5       // Sensor data corrupted.
#define SENSOR_ERROR_I2C            6       // Wrong or locked I2C address.
#define SENSOR_ERROR_GPIO_USED      7       // The GPIO is already in use.
#define SENSOR_ERROR_CALIBRATION    8       // Calibration error or not calibrated.
#define SENSOR_ERROR_OTHER          99      // Any other error.
#define CSE7766_V1R                 1.0     // 1mR current resistor.
#define CSE7766_V2R                 1.0     // 1M voltage resistor.

// Amazon echo response term & Wifi access point name.
#define ECHO_KEY_WORD "utility"

WemoManager wemoManager;
WemoSwitch *device = NULL;

SimpleTimer timer;

void setup() 
{
  // WiFiManager intialization.
  WiFiManager wifi;               

  // Initialize pins.
  pinMode( RELAY, OUTPUT );
  pinMode( LED, OUTPUT );
  pinMode( SWITCH, INPUT_PULLUP );
  delay( 10 );
  // Switch relay off, LED on.
  digitalWrite( RELAY, LOW );
  digitalWrite( LED, LOW ); 

  // Create AP, if necessary
  wifi.autoConnect( ECHO_KEY_WORD ); 
  // wemoManager used for alexa interface.
  wemoManager.begin();

  // Set WIFI module to STA mode
  WiFi.mode( WIFI_STA );
  
  // Format: Alexa invocation name, local port no, on callback, off callback
  device = new WemoSwitch( ECHO_KEY_WORD, 80, RelayOn, RelayOff );
  wemoManager.addDevice( *device );

  // Initialize Blynk.
  Blynk.config( auth );

  // Set ota details.
  ArduinoOTA.setHostname( "utility" );
  ArduinoOTA.begin();

  // Setup cse7766 serial.
  Serial.flush();
  Serial.begin( 4800 );

  // Start a timer for checking button presses @ 100ms intervals.
  timer.setInterval( 100, ButtonCheck );

  // Start a timer for checking cse7766 power monitor @ 1000ms intervals.
  timer.setInterval( 1000, ReadCse7766 );
  
  // Switch LED off to signal initialization complete.
  digitalWrite( LED, HIGH );
}

// Main program loop.
void loop() 
{
  wemoManager.serverLoop();
  ArduinoOTA.handle();
  Blynk.run();
  timer.run();
}

// Toggle the relay on
void RelayOn() 
{
  digitalWrite( RELAY, HIGH );
  relayState = true;
  Blynk.virtualWrite( V0, HIGH ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Toggle the relay off
void RelayOff() 
{
  digitalWrite( RELAY, LOW );
  relayState = false;
  Blynk.virtualWrite( V0, LOW ); // Sync the Blynk button widget state
  Blynk.virtualWrite( V1, relayState*255 );
}

// Handle switch changes originating on the Blynk app
BLYNK_WRITE( V0 ) 
{
  int SwitchStatus = param.asInt();
  
  if ( SwitchStatus )
    RelayOn();
  else 
    RelayOff();
}

// Handle timer changes originating from the Blynk app.
BLYNK_WRITE( V2 ) 
{
  int SwitchStatus = param.asInt();
  
  if ( SwitchStatus )
    RelayOn();
  else 
    RelayOff();
}

// This function runs every time Blynk connection is established.
BLYNK_CONNECTED() 
{
  if ( isFirstConnect ) 
  {
    Blynk.syncAll();
    isFirstConnect = false;
  }
}

// Handle hardware switch activation.
void ButtonCheck() 
{
  // look for new button press
  boolean SwitchState = ( digitalRead( SWITCH ) );
  
  // toggle the switch if there's a new button press
  if ( !SwitchState && SwitchReset == true ) 
  {
    if ( relayState )
      RelayOff();
    else
      RelayOn();
  
    // Flag that indicates the physical button hasn't been released
    SwitchReset = false;
    delay( 50 );            // De-bounce interlude.
  } 
  else if ( SwitchState ) 
  {
    // reset flag the physical button release
    SwitchReset = true;
  }
}

// Relay toggle helper function.
void ToggleRelay() 
{
  relayState = !relayState;
  
  if ( relayState ) 
    RelayOn();
  else 
    RelayOff();
}

// CSE7766 checksum.
bool CheckSum() 
{
  unsigned char checksum = 0;
  
  for (unsigned char i = 2; i < 23; i  ) 
    checksum  = serialBuffer[i];

  return checksum == serialBuffer[23];
}

// Process a cse7766 data packet.
void ProcessCse7766Packet() 
{
  // Confirm packet checksum.
  if ( !CheckSum() ) 
  {
    error = SENSOR_ERROR_CRC;
    return;
  }

  // Check for calibration error.
  if ( serialBuffer[0] == 0xAA ) 
  {
    error = SENSOR_ERROR_CALIBRATION;
    return;
  }
  if ( (serialBuffer[0] & 0xFC) == 0xFC ) 
  {
    error = SENSOR_ERROR_OTHER;
    return;
  }

  // Retrieve calibration coefficients.
  unsigned long coefV = (serialBuffer[2] << 16 | serialBuffer[3] << 8 | serialBuffer[4] );
  unsigned long coefC = (serialBuffer[8] << 16 | serialBuffer[9] << 8 | serialBuffer[10]);
  unsigned long coefP = (serialBuffer[14] << 16 | serialBuffer[15] << 8 | serialBuffer[16]);
  uint8_t adj = serialBuffer[20];

  // Calculate voltage.
  voltage = 0;
  if ( (adj & 0x40) == 0x40 ) 
  {
    unsigned long voltageCycle = serialBuffer[5] << 16 | serialBuffer[6] << 8 | serialBuffer[7];
    voltage = ratioV*coefV/voltageCycle/CSE7766_V2R;
  }

  // Calculate power.
  power = 0;
  if ( (adj & 0x10) == 0x10 ) 
  {
    if ( (serialBuffer[0] & 0xF2) != 0xF2 ) 
    {
      unsigned long powerCycle = serialBuffer[17] << 16 | serialBuffer[18] << 8 | serialBuffer[19];
      power = ratioP*coefP/powerCycle/CSE7766_V1R/CSE7766_V2R;
    }
  }

  // Calculate current.
  current = 0;
  if ( (adj & 0x20) == 0x20 ) 
  {
    if ( power > 0 ) 
    {
      unsigned long currentCycle = serialBuffer[11] << 16 | serialBuffer[12] << 8 | serialBuffer[13];
      current = ratioC*coefC/currentCycle/CSE7766_V1R;
    }
  }

  // Calculate energy.
  unsigned int difference;
  static unsigned int cfPulsesLast = 0;
  unsigned int cfPulses = serialBuffer[21] << 8 | serialBuffer[22];
  
  if (0 == cfPulsesLast) 
    cfPulsesLast = cfPulses;
  
  if (cfPulses < cfPulsesLast) 
    difference = cfPulses   (0xFFFF - cfPulsesLast)   1;
  else
    difference = cfPulses - cfPulsesLast;
  
  energy  = difference*(float)coefP/1000000.0;
  cfPulsesLast = cfPulses;

  // Energy reset.
  if ( power == 0 )
    energyResetCounter  ;
  else
    energyResetCounter = 0;
  if ( energyResetCounter >= MAX_ENREGY_RESET_COUNT )
  {
    energy = 0.0;
    energyResetCounter = 0;
  }
   
  // Push data to Blynk app.
  Blynk.virtualWrite( V3, voltage ); // Voltage (Volts).
  Blynk.virtualWrite( V4, current ); // Current (Amps).
  Blynk.virtualWrite( V5, power );   // Power (Watts).
  Blynk.virtualWrite( V6, energy );  // Energy (kWh).
}

// Read serial cse7766 power monitor data packet.
void ReadCse7766() 
{
  // Assume a non-specific error.
  error = SENSOR_ERROR_OTHER; 
  static unsigned char index = 0;

  while ( Serial.available() > 0 )
  {
    // A 24 byte message takes ~55ms to go through at 4800 bps.
    index = 0;

    uint8_t input = Serial.read();

    // first byte must be 0x55 or 0xF?.
    if ( index == 0 ) 
    { 
      if ( (input != 0x55) && (input < 0xF0) ) 
        continue;
    }
    // second byte must be 0x5A.
    else if ( index == 1 ) 
    {
      if ( input != 0x5A ) 
      {
        index = 0;
        continue;
      }
    }
    
    serialBuffer[index  ] = input;
    
    if ( index > 23 ) 
    {
      Serial.flush();
      break;
    }
  }

  // Process packet.
  if ( index == 24 ) 
  {
    error = SENSOR_ERROR_OK;
    ProcessCse7766Packet();
    index = 0;
  }

  // Report error state (LED) of cse7766.
  if ( error == SENSOR_ERROR_OK )
    Blynk.virtualWrite( V18, 0 );
  else
    Blynk.virtualWrite( V18, 255 );
}
Advertisements

About Jim Eli

µC experimenter
This entry was posted in arduino, iot and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s