diff --git a/.gitmodules b/.gitmodules index 087a1c97..22b9907d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "code/lib/JustWifi"] path = code/lib/JustWifi url = https://bitbucket.org/xoseperez/justwifi +[submodule "code/lib/HLW8012"] + path = code/lib/HLW8012 + url = https://bitbucket.org/xoseperez/hlw8012.git diff --git a/README.md b/README.md index 1f9234cd..0cd1ec5a 100644 --- a/README.md +++ b/README.md @@ -13,21 +13,22 @@ You can read about this board and firmware in [my blog][2]. * [ITead Sonoff RF][8] * [ITead Slampher][9] * [ITead S20 Smart Socket][10] +* [ITead Sonoff POW][15] * Tinkerman ESPurna board ## Features * **WebServer for configuration** and simple relay toggle * **Flashing firmware Over-The-Air** (OTA) -* Up to **3 configurable WIFI networks** +* Up to **3 configurable WIFI networks**, connects to the strongest signal * **MQTT support** with configurable host and topic * Manual switch ON/OFF with button (single click the button) * AP mode backup (double click the button) * Visual status of the connection via the LED -* Support for custom **[RF module][2]** * Support for **automatic over-the-air updates** through the [NoFUSS Library][6] -* Support for **current monitoring** through then [EmonLiteESP Library][7] * Support for **DHT22** sensors +* Support for the **HLW8012** power sensors present in the Sonoff POW +* Support for **current monitoring** through then [EmonLiteESP Library][7] using a non-intrusive current sensor (requires some hacking) * Command line configuration ## Installing @@ -157,3 +158,4 @@ After flashing the firmware via serial do a hard reset of the device (unplug & p [12]: https://docs.npmjs.com/getting-started/installing-node [13]: https://nodejs.org/en/ [14]: https://www.npmjs.com/ +[15]: https://www.itead.cc/sonoff-pow.html diff --git a/code/.travis.yml b/code/.travis.yml index b57f6fa8..86596a0c 100644 --- a/code/.travis.yml +++ b/code/.travis.yml @@ -24,20 +24,20 @@ # Template #1: General project. Test it using existing `platformio.ini`. # -# language: python -# python: -# - "2.7" -# -# sudo: false -# cache: -# directories: -# - "~/.platformio" -# -# install: -# - pip install -U platformio -# -# script: -# - platformio run + language: python + python: + - "2.7" + + sudo: false + cache: + directories: + - "~/.platformio" + + install: + - pip install -U platformio + + script: + - platformio run # diff --git a/code/html/custom.css b/code/html/custom.css index af368faa..d8abaf08 100644 --- a/code/html/custom.css +++ b/code/html/custom.css @@ -54,3 +54,7 @@ div.hint { color: #ccc; margin-top: -5px; } + +.module { + display: none; +} diff --git a/code/html/custom.js b/code/html/custom.js index bf859869..287f90e6 100644 --- a/code/html/custom.js +++ b/code/html/custom.js @@ -3,6 +3,7 @@ var websock; function doUpdate() { var data = $("#formSave").serializeArray(); websock.send(JSON.stringify({'config': data})); + $(".powExpected").val(0); return false; } @@ -72,6 +73,14 @@ function processData(data) { // automatic assign Object.keys(data).forEach(function(key) { + // Enable options + if (key.endsWith("Visible")) { + var module = key.slice(0,-7); + console.log(module); + $(".module-" + module).show(); + return; + } + // Look for INPUTs var element = $("input[name=" + key + "]"); if (element.length > 0) { @@ -127,7 +136,6 @@ function init() { $(".pure-menu-link").on('click', showPanel); var host = window.location.hostname; - //host = "studiolamp.local"; websock = new WebSocket('ws://' + host + ':81/'); websock.onopen = function(evt) {}; websock.onclose = function(evt) {}; diff --git a/code/html/index.html b/code/html/index.html index 08d50a11..3b82523d 100644 --- a/code/html/index.html +++ b/code/html/index.html @@ -48,7 +48,7 @@ MQTT -
  • +
  • POWER
  • @@ -112,19 +112,19 @@ -
    +
    -
    +
    -
    - - +
    + +
    @@ -276,7 +276,7 @@

    POWER

    - Configure your power monitor variables. + Calibrate your power monitor device. Use a pure resistive load and introduce the expected values for active power, current and voltage. Use the nominal values or a multimeter to get the proper numbers. Ste any field to 0 to leave the calibration value untouched.

    @@ -287,20 +287,9 @@
    - -
    This is your house nominal voltage, you probably know this or you wont be playing with this device...
    - -
    - -
    - -
    This is the value in amps for a 1V output for your sensor. Some current sensors like the YHDC SCT-013-030 have it written in the enclosure: 30A 1V. If you are using a current sensor that outputs a current (no built in burden resistor) it will depend on the turns ratio between the primary and secondary coils in the sensor and the burden resistor you use. Check about this constant in the post about calibration in the Open Energy Monitor site.
    - + +
    If you are using a pure resistive load like a bulb this will be writen on it, otherwise use a socket multimeter to get this value.
    +
    diff --git a/code/lib/HLW8012 b/code/lib/HLW8012 new file mode 160000 index 00000000..1dfa68b9 --- /dev/null +++ b/code/lib/HLW8012 @@ -0,0 +1 @@ +Subproject commit 1dfa68b9e79a4a8ea96df4ed20f64e0c12ee9d26 diff --git a/code/platformio.custom.ini b/code/platformio.custom.ini index f5e66534..f57c87a9 100644 --- a/code/platformio.custom.ini +++ b/code/platformio.custom.ini @@ -12,7 +12,7 @@ env_default = node-debug [common] platform = espressif8266 framework = arduino -lib_install = 19,44,64,89,549,727 +lib_install = 19,31,44,64,89,549,727 extra_script = pio_hooks.py [ota] @@ -28,6 +28,14 @@ build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF [env:sonoff-debug-ota] include = env:sonoff-debug,ota +[env:sonoff-pow-debug] +include = common +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF_POW + +[env:sonoff-pow-debug-ota] +include = env:sonoff-pow-debug,ota + [env:slampher-debug] include = common board = esp01_1m @@ -71,4 +79,4 @@ include = env:sonoff-debug-ota [env:living-lamp-device] topic = /home/living/lamp/ip -include = env:sonoff-debug-ota +include = env:s20-debug-ota diff --git a/code/platformio.ini b/code/platformio.ini index fa07d532..8400b7c6 100644 --- a/code/platformio.ini +++ b/code/platformio.ini @@ -1,5 +1,5 @@ [platformio] -env_default = node-debug +env_default = sonoff-pow-debug [env:sonoff-debug] platform = espressif8266 @@ -20,6 +20,14 @@ upload_speed = 115200 upload_port = "192.168.4.1" upload_flags = --auth=fibonacci --port 8266 +[env:sonoff-pow-debug] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF_POW + [env:slampher-debug] platform = espressif8266 framework = arduino diff --git a/code/platformio.official.ini b/code/platformio.official.ini new file mode 100644 index 00000000..8400b7c6 --- /dev/null +++ b/code/platformio.official.ini @@ -0,0 +1,136 @@ +[platformio] +env_default = sonoff-pow-debug + +[env:sonoff-debug] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF + +[env:sonoff-debug-ota] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 + +[env:sonoff-pow-debug] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF_POW + +[env:slampher-debug] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSLAMPHER + +[env:slampher-debug-ota] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSLAMPHER +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 + +[env:s20-debug] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DS20 + +[env:s20-debug-ota] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DS20 +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 + +[env:node-debug] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = nodemcuv2 +build_flags = -DNODEMCUV2 -DDEBUG_PORT=Serial + +[env:node-debug-ota] +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = nodemcuv2 +build_flags = -DNODEMCUV2 -DDEBUG_PORT=Serial +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 + + + +[env:ac-device] +topic = /home/cellar/airconditioner/ip +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DS20 +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 + +[env:washer-device] +topic = /home/cellar/washer/ip +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF -DENABLE_EMON=1 -DENABLE_DHT=1 + +[env:studio-lamp-device] +topic = /home/studio/lamp/ip +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 + +[env:living-lamp-device] +topic = /home/living/lamp/ip +platform = espressif8266 +framework = arduino +lib_install = 19,31,44,64,89,549,727 +extra_script = pio_hooks.py +board = esp01_1m +build_flags = -Wl,-Tesp8266.flash.1m256.ld -DDEBUG_PORT=Serial -DSONOFF +upload_speed = 115200 +upload_port = "192.168.4.1" +upload_flags = --auth=fibonacci --port 8266 diff --git a/code/src/defaults.h b/code/src/defaults.h index bf18832e..996c05f3 100644 --- a/code/src/defaults.h +++ b/code/src/defaults.h @@ -14,6 +14,7 @@ //#define ENABLE_EMON 1 //#define ENABLE_DHT 1 //#define ENABLE_RF 1 +//#define ENABLE_POW 1 // ----------------------------------------------------------------------------- // HARDWARE @@ -35,6 +36,13 @@ #define LED_PIN 13 #endif +#ifdef SONOFF_POW + #define ENABLE_POW 1 + #define MANUFACTURER "ITEAD" + #define DEVICE "SONOFF_POW" + #define LED_PIN 13 +#endif + #ifdef SLAMPHER #define MANUFACTURER "ITEAD" #define DEVICE "SLAMPHER" @@ -124,3 +132,14 @@ #define EMON_MAINS_VOLTAGE 230 #define EMON_CURRENT_RATIO 180 #define EMON_POWER_TOPIC "/power" + +#define POW_SEL_PIN 5 +#define POW_CF1_PIN 13 +#define POW_CF_PIN 14 +#define POW_SEL_CURRENT HIGH +#define POW_CURRENT_R 0.001 +#define POW_VOLTAGE_R_UP ( 5 * 470000 ) // Real: 2280k +#define POW_VOLTAGE_R_DOWN ( 1000 ) // Real 1.009k +#define POW_POWER_TOPIC "/power" +#define POW_UPDATE_INTERVAL 10000 +#define POW_REPORT_EVERY 6 diff --git a/code/src/dht.ino b/code/src/dht.ino index a1af8d2a..fe412fc2 100644 --- a/code/src/dht.ino +++ b/code/src/dht.ino @@ -64,8 +64,8 @@ void dhtLoop() { mqttSend((char *) getSetting("dhtHumTopic", DHT_HUMIDITY_TOPIC).c_str(), humidity); // Update websocket clients - char buffer[20]; - sprintf_P(buffer, PSTR("{\"dhtTmp\": %s, \"dhtHum\": %s}"), temperature, humidity); + char buffer[100]; + sprintf_P(buffer, PSTR("{\"dhtVisible\": 1, \"dhtTmp\": %s, \"dhtHum\": %s}"), temperature, humidity); webSocketSend(buffer); } diff --git a/code/src/main.ino b/code/src/main.ino index 6d6e81e0..26b91b36 100644 --- a/code/src/main.ino +++ b/code/src/main.ino @@ -151,6 +151,9 @@ void setup() { #if ENABLE_NOFUSS nofussSetup(); #endif + #if ENABLE_POW + powSetup(); + #endif #if ENABLE_DHT dhtSetup(); #endif @@ -178,6 +181,9 @@ void loop() { #if ENABLE_NOFUSS nofussLoop(); #endif + #if ENABLE_POW + powLoop(); + #endif #if ENABLE_DHT dhtLoop(); #endif diff --git a/code/src/pow.ino b/code/src/pow.ino new file mode 100644 index 00000000..93260332 --- /dev/null +++ b/code/src/pow.ino @@ -0,0 +1,160 @@ +/* + +ESPurna +POW MODULE +Support for Sonoff POW HLW8012-based power monitor + +Copyright (C) 2016 by Xose Pérez + +*/ + +#if ENABLE_POW + +#include + +HLW8012 hlw8012; + +// ----------------------------------------------------------------------------- +// POW +// ----------------------------------------------------------------------------- + +// When using interrupts we have to call the library entry point +// whenever an interrupt is triggered +void hlw8012_cf1_interrupt() { + hlw8012.cf1_interrupt(); +} + +void hlw8012_cf_interrupt() { + hlw8012.cf_interrupt(); +} + +void powAttachInterrupts() { + //attachInterrupt(POW_CF1_PIN, hlw8012_cf1_interrupt, CHANGE); + attachInterrupt(POW_CF_PIN, hlw8012_cf_interrupt, CHANGE); + DEBUG_MSG("[POW] Enabled\n"); +} + +void powDettachInterrupts() { + //detachInterrupt(POW_CF1_PIN); + detachInterrupt(POW_CF_PIN); + DEBUG_MSG("[POW] Disabled\n"); +} + +void powSaveCalibration() { + setSetting("powPowerMult", String() + hlw8012.getPowerMultiplier()); + setSetting("powCurrentMult", String() + hlw8012.getCurrentMultiplier()); + setSetting("powVoltageMult", String() + hlw8012.getVoltageMultiplier()); +} + +void powRetrieveCalibration() { + double value; + value = getSetting("powPowerMult", "0").toFloat(); + if (value > 0) hlw8012.setPowerMultiplier((int) value); + value = getSetting("powCurrentMult", "0").toFloat(); + if (value > 0) hlw8012.setCurrentMultiplier((int) value); + value = getSetting("powVoltageMult", "0").toFloat(); + if (value > 0) hlw8012.setVoltageMultiplier((int) value); +} + +void powSetExpectedActivePower(unsigned int power) { + if (power > 0) { + hlw8012.expectedActivePower(power); + powSaveCalibration(); + } +} + +void powSetExpectedCurrent(double current) { + if (current > 0) { + hlw8012.expectedCurrent(current); + powSaveCalibration(); + } +} + +void powSetExpectedVoltage(unsigned int voltage) { + if (voltage > 0) { + hlw8012.expectedVoltage(voltage); + powSaveCalibration(); + } +} + +unsigned int getActivePower() { + return hlw8012.getActivePower(); +} + +unsigned int getApparentPower() { + return hlw8012.getApparentPower(); +} + +double getCurrent() { + return hlw8012.getCurrent(); +} + +unsigned int getVoltage() { + return hlw8012.getVoltage(); +} + +unsigned int getPowerFactor() { + return (int) (100 * hlw8012.getPowerFactor()); +} + +void powSetup() { + + // Initialize HLW8012 + // void begin(unsigned char cf_pin, unsigned char cf1_pin, unsigned char sel_pin, unsigned char currentWhen = HIGH, bool use_interrupts = false, unsigned long pulse_timeout = PULSE_TIMEOUT); + // * cf_pin, cf1_pin and sel_pin are GPIOs to the HLW8012 IC + // * currentWhen is the value in sel_pin to select current sampling + // * set use_interrupts to true to use interrupts to monitor pulse widths + // * leave pulse_timeout to the default value, recommended when using interrupts + hlw8012.begin(POW_CF_PIN, POW_CF1_PIN, POW_SEL_PIN, POW_SEL_CURRENT, true); + + // These values are used to calculate current, voltage and power factors as per datasheet formula + // These are the nominal values for the Sonoff POW resistors: + // * The CURRENT_RESISTOR is the 1milliOhm copper-manganese resistor in series with the main line + // * The VOLTAGE_RESISTOR_UPSTREAM are the 5 470kOhm resistors in the voltage divider that feeds the V2P pin in the HLW8012 + // * The VOLTAGE_RESISTOR_DOWNSTREAM is the 1kOhm resistor in the voltage divider that feeds the V2P pin in the HLW8012 + hlw8012.setResistors(POW_CURRENT_R, POW_VOLTAGE_R_UP, POW_VOLTAGE_R_DOWN); + + powRetrieveCalibration(); + + static WiFiEventHandler e1 = WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected& event) { + powDettachInterrupts(); + }); + + static WiFiEventHandler e2 = WiFi.onSoftAPModeStationDisconnected([](const WiFiEventSoftAPModeStationDisconnected& event) { + powDettachInterrupts(); + }); + + static WiFiEventHandler e3 = WiFi.onStationModeConnected([](const WiFiEventStationModeConnected& event) { + powAttachInterrupts(); + }); + + static WiFiEventHandler e4 = WiFi.onSoftAPModeStationConnected([](const WiFiEventSoftAPModeStationConnected& event) { + powAttachInterrupts(); + }); + +} + +void powLoop() { + + static unsigned long last_update = 0; + static unsigned char report_count = POW_REPORT_EVERY; + + if ((millis() - last_update > POW_UPDATE_INTERVAL) || (last_update == 0 )){ + last_update = millis(); + + unsigned int power = getActivePower(); + + char buffer[100]; + sprintf_P(buffer, PSTR("{\"powVisible\": 1, \"powActivePower\": %d}"), power); + webSocketSend(buffer); + + if (--report_count == 0) { + mqttSend((char *) getSetting("powPowerTopic", POW_POWER_TOPIC).c_str(), (char *) String(power).c_str()); + report_count = POW_REPORT_EVERY; + } + + } + +} + +#endif diff --git a/code/src/version.h b/code/src/version.h index 63d924f3..624ada9e 100644 --- a/code/src/version.h +++ b/code/src/version.h @@ -1,4 +1,4 @@ #define APP_NAME "Espurna" -#define APP_VERSION "0.9.8" +#define APP_VERSION "0.9.9" #define APP_AUTHOR "xose.perez@gmail.com" #define APP_WEBSITE "http://tinkerman.cat" diff --git a/code/src/websockets.ino b/code/src/websockets.ino index f2bfa48b..8bab19be 100644 --- a/code/src/websockets.ino +++ b/code/src/websockets.ino @@ -67,6 +67,15 @@ void webSocketParse(uint8_t num, uint8_t * payload, size_t length) { String key = config[i]["name"]; String value = config[i]["value"]; + #if ENABLE_POW + + if (key == "powExpectedPower") { + powSetExpectedActivePower(value.toInt()); + continue; + } + + #endif + if (key == "ssid") { key = key + String(network); } @@ -137,21 +146,29 @@ void webSocketStart(uint8_t num) { root["relayMode"] = getSetting("relayMode", String(RELAY_MODE)); #if ENABLE_DHT + root["dhtVisible"] = 1; root["dhtTmp"] = getTemperature(); root["dhtHum"] = getHumidity(); #endif #if ENABLE_RF + root["rfVisible"] = 1; root["rfChannel"] = getSetting("rfChannel", String(RF_CHANNEL)); root["rfDevice"] = getSetting("rfDevice", String(RF_DEVICE)); #endif #if ENABLE_EMON + root["emonVisible"] = 1; root["emonPower"] = getPower(); root["emonMains"] = getSetting("emonMains", String(EMON_MAINS_VOLTAGE)); root["emonRatio"] = getSetting("emonRatio", String(EMON_CURRENT_RATIO)); #endif + #if ENABLE_POW + root["powVisible"] = 1; + root["powActivePower"] = getActivePower(); + #endif + JsonArray& wifi = root.createNestedArray("wifi"); for (byte i=0; i<3; i++) { JsonObject& network = wifi.createNestedObject();