@ -1,4 +1,4 @@ | |||||
#define APP_NAME "ESPURNA" | #define APP_NAME "ESPURNA" | ||||
#define APP_VERSION "1.9.4b" | |||||
#define APP_VERSION "1.9.5" | |||||
#define APP_AUTHOR "xose.perez@gmail.com" | #define APP_AUTHOR "xose.perez@gmail.com" | ||||
#define APP_WEBSITE "http://tinkerman.cat" | #define APP_WEBSITE "http://tinkerman.cat" |
@ -1,217 +0,0 @@ | |||||
/* | |||||
EMON MODULE | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if EMON_SUPPORT | |||||
#include <EmonLiteESP.h> | |||||
#include <EEPROM.h> | |||||
#if EMON_PROVIDER == EMON_ADC121_PROVIDER | |||||
#include "brzo_i2c.h" | |||||
#endif | |||||
// ADC121 Registers | |||||
#define ADC121_REG_RESULT 0x00 | |||||
#define ADC121_REG_ALERT 0x01 | |||||
#define ADC121_REG_CONFIG 0x02 | |||||
#define ADC121_REG_LIMITL 0x03 | |||||
#define ADC121_REG_LIMITH 0x04 | |||||
#define ADC121_REG_HYST 0x05 | |||||
#define ADC121_REG_CONVL 0x06 | |||||
#define ADC121_REG_CONVH 0x07 | |||||
EmonLiteESP emon; | |||||
bool _emonReady = false; | |||||
double _emonCurrent = 0; | |||||
unsigned int _emonPower = 0; | |||||
unsigned int _emonVoltage = 0; | |||||
// ----------------------------------------------------------------------------- | |||||
// Provider | |||||
// ----------------------------------------------------------------------------- | |||||
unsigned int currentCallback() { | |||||
#if EMON_PROVIDER == EMON_ANALOG_PROVIDER | |||||
return analogRead(EMON_CURRENT_PIN); | |||||
#endif | |||||
#if EMON_PROVIDER == EMON_ADC121_PROVIDER | |||||
uint8_t buffer[2]; | |||||
brzo_i2c_start_transaction(EMON_ADC121_ADDRESS, I2C_SCL_FREQUENCY); | |||||
buffer[0] = ADC121_REG_RESULT; | |||||
brzo_i2c_write(buffer, 1, false); | |||||
brzo_i2c_read(buffer, 2, false); | |||||
brzo_i2c_end_transaction(); | |||||
unsigned int value; | |||||
value = (buffer[0] & 0x0F) << 8; | |||||
value |= buffer[1]; | |||||
return value; | |||||
#endif | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// HAL | |||||
// ----------------------------------------------------------------------------- | |||||
void setCurrentRatio(float value) { | |||||
emon.setCurrentRatio(value); | |||||
} | |||||
unsigned int getApparentPower() { | |||||
return int(getCurrent() * getVoltage()); | |||||
} | |||||
double getCurrent() { | |||||
double current = emon.getCurrent(EMON_SAMPLES); | |||||
current -= EMON_CURRENT_OFFSET; | |||||
if (current < 0) current = 0; | |||||
return current; | |||||
} | |||||
unsigned int getVoltage() { | |||||
return getSetting("emonVoltage", EMON_MAINS_VOLTAGE).toInt(); | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
void powerMonitorSetup() { | |||||
// backwards compatibility | |||||
String tmp; | |||||
moveSetting("pwMainsVoltage", "emonVoltage"); | |||||
moveSetting("emonMains", "emonVoltage"); | |||||
moveSetting("pwCurrentRatio", "emonRatio"); | |||||
emon.initCurrent( | |||||
currentCallback, | |||||
EMON_ADC_BITS, | |||||
EMON_REFERENCE_VOLTAGE, | |||||
getSetting("emonRatio", EMON_CURRENT_RATIO).toFloat() | |||||
); | |||||
#if EMON_PROVIDER == EMON_ADC121_PROVIDER | |||||
uint8_t buffer[2]; | |||||
buffer[0] = ADC121_REG_CONFIG; | |||||
buffer[1] = 0x00; | |||||
brzo_i2c_start_transaction(EMON_ADC121_ADDRESS, I2C_SCL_FREQUENCY); | |||||
brzo_i2c_write(buffer, 2, false); | |||||
brzo_i2c_end_transaction(); | |||||
#endif | |||||
#if WEB_SUPPORT | |||||
apiRegister(EMON_APOWER_TOPIC, EMON_APOWER_TOPIC, [](char * buffer, size_t len) { | |||||
if (_emonReady) { | |||||
snprintf_P(buffer, len, PSTR("%d"), _emonPower); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
apiRegister(EMON_CURRENT_TOPIC, EMON_CURRENT_TOPIC, [](char * buffer, size_t len) { | |||||
if (_emonReady) { | |||||
dtostrf(_emonCurrent, len-1, 3, buffer); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
#endif // WEB_SUPPORT | |||||
} | |||||
void powerMonitorLoop() { | |||||
static unsigned long next_measurement = millis(); | |||||
static bool warmup = true; | |||||
static byte measurements = 0; | |||||
static double max = 0; | |||||
static double min = 0; | |||||
static double sum = 0; | |||||
if (warmup) { | |||||
warmup = false; | |||||
emon.warmup(); | |||||
} | |||||
if (millis() > next_measurement) { | |||||
int voltage = getVoltage(); | |||||
{ | |||||
double current = getCurrent(); | |||||
if (measurements == 0) { | |||||
max = min = current; | |||||
} else { | |||||
if (_emonCurrent > max) max = current; | |||||
if (_emonCurrent < min) min = current; | |||||
} | |||||
sum += current; | |||||
++measurements; | |||||
DEBUG_MSG_P(PSTR("[ENERGY] Current: %sA\n"), String(current, 3).c_str()); | |||||
DEBUG_MSG_P(PSTR("[ENERGY] Power: %dW\n"), int(current * voltage)); | |||||
// Update websocket clients | |||||
#if WEB_SUPPORT | |||||
char buffer[100]; | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("{\"emonVisible\": 1, \"emonApparentPower\": %d, \"emonCurrent\": %s}"), int(current * voltage), String(current, 3).c_str()); | |||||
wsSend(buffer); | |||||
#endif | |||||
} | |||||
// Send MQTT messages averaged every EMON_MEASUREMENTS | |||||
if (measurements == EMON_MEASUREMENTS) { | |||||
// Calculate average current (removing max and min values) | |||||
_emonCurrent = (sum - max - min) / (measurements - 2); | |||||
_emonPower = (int) (_emonCurrent * voltage); | |||||
_emonReady = true; | |||||
// Calculate energy increment (ppower times time) | |||||
double energy_delta = (double) _emonPower * EMON_INTERVAL * EMON_MEASUREMENTS / 1000.0 / 3600.0; | |||||
// Report values to MQTT broker | |||||
mqttSend(getSetting("emonPowerTopic", EMON_APOWER_TOPIC).c_str(), String(_emonPower).c_str()); | |||||
mqttSend(getSetting("emonCurrTopic", EMON_CURRENT_TOPIC).c_str(), String(_emonCurrent, 3).c_str()); | |||||
mqttSend(getSetting("emonEnergyTopic", EMON_ENERGY_TOPIC).c_str(), String(energy_delta, 3).c_str()); | |||||
// Report values to Domoticz | |||||
#if DOMOTICZ_SUPPORT | |||||
{ | |||||
char buffer[20]; | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%d;%s"), _emonPower, String(energy_delta, 3).c_str()); | |||||
domoticzSend("dczPowIdx", 0, buffer); | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%s"), String(energy_delta, 3).c_str()); | |||||
domoticzSend("dczEnergyIdx", 0, buffer); | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%s"), String(_emonCurrent, 3).c_str()); | |||||
domoticzSend("dczCurrentIdx", 0, buffer); | |||||
} | |||||
#endif | |||||
#if INFLUXDB_SUPPORT | |||||
influxDBSend(getSetting("emonPowerTopic", EMON_APOWER_TOPIC).c_str(), _emonPower); | |||||
influxDBSend(getSetting("emonCurrTopic", EMON_CURRENT_TOPIC).c_str(), String(_emonCurrent, 3).c_str()); | |||||
influxDBSend(getSetting("emonEnergyTopic", EMON_ENERGY_TOPIC).c_str(), String(energy_delta, 3).c_str()); | |||||
#endif | |||||
// Reset counters | |||||
sum = measurements = 0; | |||||
} | |||||
next_measurement += EMON_INTERVAL; | |||||
} | |||||
} | |||||
#endif |
@ -1,344 +0,0 @@ | |||||
/* | |||||
POW MODULE | |||||
Support for Sonoff POW HLW8012-based power monitor | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if HLW8012_SUPPORT | |||||
#include <HLW8012.h> | |||||
#include <Hash.h> | |||||
#include <ArduinoJson.h> | |||||
HLW8012 hlw8012; | |||||
bool _hlw8012Enabled = false; | |||||
bool _hlwReady = false; | |||||
int _hlwPower = 0; | |||||
double _hlwCurrent = 0; | |||||
int _hlwVoltage = 0; | |||||
// ----------------------------------------------------------------------------- | |||||
// POW | |||||
// ----------------------------------------------------------------------------- | |||||
// When using interrupts we have to call the library entry point | |||||
// whenever an interrupt is triggered | |||||
void ICACHE_RAM_ATTR hlw8012_cf1_interrupt() { | |||||
hlw8012.cf1_interrupt(); | |||||
} | |||||
void ICACHE_RAM_ATTR hlw8012_cf_interrupt() { | |||||
hlw8012.cf_interrupt(); | |||||
} | |||||
void hlw8012Enable(bool status) { | |||||
_hlw8012Enabled = status; | |||||
if (_hlw8012Enabled) { | |||||
#if HLW8012_USE_INTERRUPTS == 1 | |||||
attachInterrupt(HLW8012_CF1_PIN, hlw8012_cf1_interrupt, CHANGE); | |||||
attachInterrupt(HLW8012_CF_PIN, hlw8012_cf_interrupt, CHANGE); | |||||
#endif | |||||
DEBUG_MSG_P(PSTR("[POW] Enabled\n")); | |||||
} else { | |||||
#if HLW8012_USE_INTERRUPTS == 1 | |||||
detachInterrupt(HLW8012_CF1_PIN); | |||||
detachInterrupt(HLW8012_CF_PIN); | |||||
#endif | |||||
DEBUG_MSG_P(PSTR("[POW] Disabled\n")); | |||||
} | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
void hlw8012SaveCalibration() { | |||||
setSetting("powPowerMult", hlw8012.getPowerMultiplier()); | |||||
setSetting("powCurrentMult", hlw8012.getCurrentMultiplier()); | |||||
setSetting("powVoltageMult", hlw8012.getVoltageMultiplier()); | |||||
saveSettings(); | |||||
} | |||||
void hlw8012RetrieveCalibration() { | |||||
double value; | |||||
value = getSetting("powPowerMult", 0).toFloat(); | |||||
if (value > 0) hlw8012.setPowerMultiplier(value); | |||||
value = getSetting("powCurrentMult", 0).toFloat(); | |||||
if (value > 0) hlw8012.setCurrentMultiplier(value); | |||||
value = getSetting("powVoltageMult", 0).toFloat(); | |||||
if (value > 0) hlw8012.setVoltageMultiplier(value); | |||||
} | |||||
void hlw8012SetExpectedActivePower(unsigned int power) { | |||||
if (power > 0) { | |||||
hlw8012.expectedActivePower(power); | |||||
hlw8012SaveCalibration(); | |||||
} | |||||
} | |||||
void hlw8012SetExpectedCurrent(double current) { | |||||
if (current > 0) { | |||||
hlw8012.expectedCurrent(current); | |||||
hlw8012SaveCalibration(); | |||||
} | |||||
} | |||||
void hlw8012SetExpectedVoltage(unsigned int voltage) { | |||||
if (voltage > 0) { | |||||
hlw8012.expectedVoltage(voltage); | |||||
hlw8012SaveCalibration(); | |||||
} | |||||
} | |||||
void hlw8012Reset() { | |||||
hlw8012.resetMultipliers(); | |||||
hlw8012SaveCalibration(); | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// HAL | |||||
// ----------------------------------------------------------------------------- | |||||
unsigned int getActivePower() { | |||||
unsigned int power = hlw8012.getActivePower(); | |||||
if (HLW8012_MIN_POWER > power || power > HLW8012_MAX_POWER) power = 0; | |||||
return power; | |||||
} | |||||
unsigned int getApparentPower() { | |||||
unsigned int power = hlw8012.getApparentPower(); | |||||
if (HLW8012_MIN_POWER > power || power > HLW8012_MAX_POWER) power = 0; | |||||
return power; | |||||
} | |||||
unsigned int getReactivePower() { | |||||
unsigned int power = hlw8012.getReactivePower(); | |||||
if (HLW8012_MIN_POWER > power || power > HLW8012_MAX_POWER) power = 0; | |||||
return power; | |||||
} | |||||
double getCurrent() { | |||||
double current = hlw8012.getCurrent(); | |||||
if (HLW8012_MIN_CURRENT > current || current > HLW8012_MAX_CURRENT) current = 0; | |||||
return current; | |||||
} | |||||
unsigned int getVoltage() { | |||||
return hlw8012.getVoltage(); | |||||
} | |||||
double getPowerFactor() { | |||||
return hlw8012.getPowerFactor(); | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
void hlw8012Setup() { | |||||
// 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 | |||||
#if HLW8012_USE_INTERRUPTS | |||||
hlw8012.begin(HLW8012_CF_PIN, HLW8012_CF1_PIN, HLW8012_SEL_PIN, HLW8012_SEL_CURRENT, true); | |||||
#else | |||||
hlw8012.begin(HLW8012_CF_PIN, HLW8012_CF1_PIN, HLW8012_SEL_PIN, HLW8012_SEL_CURRENT, false, 1000000); | |||||
#endif | |||||
// 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(HLW8012_CURRENT_R, HLW8012_VOLTAGE_R_UP, HLW8012_VOLTAGE_R_DOWN); | |||||
// Retrieve calibration values | |||||
hlw8012RetrieveCalibration(); | |||||
// API definitions | |||||
#if WEB_SUPPORT | |||||
apiRegister(HLW8012_POWER_TOPIC, HLW8012_POWER_TOPIC, [](char * buffer, size_t len) { | |||||
if (_hlwReady) { | |||||
snprintf_P(buffer, len, PSTR("%d"), _hlwPower); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
apiRegister(HLW8012_CURRENT_TOPIC, HLW8012_CURRENT_TOPIC, [](char * buffer, size_t len) { | |||||
if (_hlwReady) { | |||||
dtostrf(_hlwCurrent, len-1, 3, buffer); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
apiRegister(HLW8012_VOLTAGE_TOPIC, HLW8012_VOLTAGE_TOPIC, [](char * buffer, size_t len) { | |||||
if (_hlwReady) { | |||||
snprintf_P(buffer, len, PSTR("%d"), _hlwVoltage); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
#endif // WEB_SUPPORT | |||||
} | |||||
void hlw8012Loop() { | |||||
static unsigned long last_update = 0; | |||||
static unsigned char report_count = HLW8012_REPORT_EVERY; | |||||
static bool power_spike = false; | |||||
static unsigned long power_sum = 0; | |||||
static unsigned long power_previous = 0; | |||||
static bool current_spike = false; | |||||
static double current_sum = 0; | |||||
static double current_previous = 0; | |||||
static bool voltage_spike = false; | |||||
static unsigned long voltage_sum = 0; | |||||
static unsigned long voltage_previous = 0; | |||||
static bool powWasEnabled = false; | |||||
// POW is disabled while there is no internet connection | |||||
// When the HLW8012 measurements are enabled back we reset the timer | |||||
if (!_hlw8012Enabled) { | |||||
powWasEnabled = false; | |||||
return; | |||||
} | |||||
if (!powWasEnabled) { | |||||
last_update = millis(); | |||||
powWasEnabled = true; | |||||
} | |||||
if (millis() - last_update > HLW8012_UPDATE_INTERVAL) { | |||||
last_update = millis(); | |||||
unsigned int power = getActivePower(); | |||||
unsigned int voltage = getVoltage(); | |||||
double current = getCurrent(); | |||||
if (power > 0) { | |||||
power_spike = (power_previous == 0); | |||||
} else if (power_spike) { | |||||
power_sum -= power_previous; | |||||
power_spike = false; | |||||
} | |||||
power_previous = power; | |||||
if (current > 0) { | |||||
current_spike = (current_previous == 0); | |||||
} else if (current_spike) { | |||||
current_sum -= current_previous; | |||||
current_spike = false; | |||||
} | |||||
current_previous = current; | |||||
if (voltage > 0) { | |||||
voltage_spike = (voltage_previous == 0); | |||||
} else if (voltage_spike) { | |||||
voltage_sum -= voltage_previous; | |||||
voltage_spike = false; | |||||
} | |||||
voltage_previous = voltage; | |||||
#if WEB_SUPPORT | |||||
{ | |||||
unsigned int apparent = getApparentPower(); | |||||
double factor = getPowerFactor(); | |||||
unsigned int reactive = getReactivePower(); | |||||
DynamicJsonBuffer jsonBuffer; | |||||
JsonObject& root = jsonBuffer.createObject(); | |||||
root["powVisible"] = 1; | |||||
root["powActivePower"] = power; | |||||
root["powCurrent"] = String(current, 3); | |||||
root["powVoltage"] = voltage; | |||||
root["powApparentPower"] = apparent; | |||||
root["powReactivePower"] = reactive; | |||||
root["powPowerFactor"] = String(factor, 2); | |||||
String output; | |||||
root.printTo(output); | |||||
wsSend(output.c_str()); | |||||
} | |||||
#endif | |||||
if (--report_count == 0) { | |||||
// Update globals | |||||
_hlwPower = power_sum / HLW8012_REPORT_EVERY; | |||||
_hlwCurrent = current_sum / HLW8012_REPORT_EVERY; | |||||
_hlwVoltage = voltage_sum / HLW8012_REPORT_EVERY; | |||||
_hlwReady = true; | |||||
// Calculate subproducts (apparent and reactive power, power factor and delta energy) | |||||
unsigned int apparent = _hlwCurrent * _hlwVoltage; | |||||
unsigned int reactive = (apparent > _hlwPower) ? sqrt(apparent * apparent - _hlwPower * _hlwPower) : 0; | |||||
double factor = (apparent > 0) ? (double) _hlwPower / apparent : 1; | |||||
if (factor > 1) factor = 1; | |||||
double energy_delta = (double) _hlwPower * HLW8012_REPORT_EVERY * HLW8012_UPDATE_INTERVAL / 1000.0 / 3600.0; | |||||
// Report values to MQTT broker | |||||
mqttSend(getSetting("powPowerTopic", HLW8012_POWER_TOPIC).c_str(), String(_hlwPower).c_str()); | |||||
mqttSend(getSetting("powCurrentTopic", HLW8012_CURRENT_TOPIC).c_str(), String(_hlwCurrent, 3).c_str()); | |||||
mqttSend(getSetting("powVoltageTopic", HLW8012_VOLTAGE_TOPIC).c_str(), String(_hlwVoltage).c_str()); | |||||
mqttSend(getSetting("powEnergyTopic", HLW8012_ENERGY_TOPIC).c_str(), String(energy_delta, 3).c_str()); | |||||
mqttSend(getSetting("powAPowerTopic", HLW8012_APOWER_TOPIC).c_str(), String(apparent).c_str()); | |||||
mqttSend(getSetting("powRPowerTopic", HLW8012_RPOWER_TOPIC).c_str(), String(reactive).c_str()); | |||||
mqttSend(getSetting("powPFactorTopic", HLW8012_PFACTOR_TOPIC).c_str(), String(factor, 2).c_str()); | |||||
// Report values to Domoticz | |||||
#if DOMOTICZ_SUPPORT | |||||
{ | |||||
char buffer[20]; | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%d;%s"), _hlwPower, String(energy_delta, 3).c_str()); | |||||
domoticzSend("dczPowIdx", 0, buffer); | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%s"), String(energy_delta, 3).c_str()); | |||||
domoticzSend("dczEnergyIdx", 0, buffer); | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _hlwVoltage); | |||||
domoticzSend("dczVoltIdx", 0, buffer); | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%s"), String(_hlwCurrent).c_str()); | |||||
domoticzSend("dczCurrentIdx", 0, buffer); | |||||
} | |||||
#endif | |||||
#if INFLUXDB_SUPPORT | |||||
influxDBSend(getSetting("powPowerTopic", HLW8012_POWER_TOPIC).c_str(), String(_hlwPower).c_str()); | |||||
influxDBSend(getSetting("powCurrentTopic", HLW8012_CURRENT_TOPIC).c_str(), String(_hlwCurrent, 3).c_str()); | |||||
influxDBSend(getSetting("powVoltageTopic", HLW8012_VOLTAGE_TOPIC).c_str(), String(_hlwVoltage).c_str()); | |||||
influxDBSend(getSetting("powEnergyTopic", HLW8012_ENERGY_TOPIC).c_str(), String(energy_delta, 3).c_str()); | |||||
influxDBSend(getSetting("powAPowerTopic", HLW8012_APOWER_TOPIC).c_str(), String(apparent).c_str()); | |||||
influxDBSend(getSetting("powRPowerTopic", HLW8012_RPOWER_TOPIC).c_str(), String(reactive).c_str()); | |||||
influxDBSend(getSetting("powPFactorTopic", HLW8012_PFACTOR_TOPIC).c_str(), String(factor, 2).c_str()); | |||||
#endif | |||||
// Reset counters | |||||
power_sum = current_sum = voltage_sum = 0; | |||||
report_count = HLW8012_REPORT_EVERY; | |||||
} | |||||
// Post - Accumulators | |||||
power_sum += power_previous; | |||||
current_sum += current_previous; | |||||
voltage_sum += voltage_previous; | |||||
// Toggle between current and voltage monitoring | |||||
#if HLW8012_USE_INTERRUPTS == 0 | |||||
hlw8012.toggleMode(); | |||||
#endif | |||||
} | |||||
} | |||||
#endif |
@ -0,0 +1,70 @@ | |||||
// ----------------------------------------------------------------------------- | |||||
// Stream Injector | |||||
// ----------------------------------------------------------------------------- | |||||
#pragma once | |||||
class MedianFilter { | |||||
public: | |||||
MedianFilter(unsigned char size) { | |||||
_size = size; | |||||
_data = new double[_size+1]; | |||||
reset(); | |||||
} | |||||
virtual void reset() { | |||||
if (_pointer > 1) _data[0] = _data[_pointer-1]; | |||||
_pointer = 1; | |||||
for (unsigned char i=1; i<=_size; i++) _data[i] = 0; | |||||
} | |||||
virtual void add(double value) { | |||||
if (_pointer <= _size) { | |||||
_data[_pointer] = value; | |||||
++_pointer; | |||||
} | |||||
} | |||||
virtual double average(bool do_reset = false) { | |||||
double sum = 0; | |||||
if (_pointer > 2) { | |||||
for (unsigned char i = 1; i<_pointer-1; i++) { | |||||
double previous = _data[i-1]; | |||||
double current = _data[i]; | |||||
double next = _data[i+1]; | |||||
if (previous > current) std::swap(previous, current); | |||||
if (current > next) std::swap(current, next); | |||||
if (previous > current) std::swap(previous, current); | |||||
sum += current; | |||||
} | |||||
sum /= (_pointer - 2); | |||||
} | |||||
if (do_reset) reset(); | |||||
return sum; | |||||
} | |||||
virtual unsigned char count() { | |||||
return _pointer - 1; | |||||
} | |||||
private: | |||||
double *_data; | |||||
unsigned char _size = 0; | |||||
unsigned char _pointer = 0; | |||||
}; |
@ -0,0 +1,334 @@ | |||||
/* | |||||
POWER MODULE | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if POWER_PROVIDER != POWER_PROVIDER_NONE | |||||
// ----------------------------------------------------------------------------- | |||||
// MODULE GLOBALS AND CACHE | |||||
// ----------------------------------------------------------------------------- | |||||
#include "power.h" | |||||
#include <Hash.h> | |||||
#include <ArduinoJson.h> | |||||
bool _power_enabled = false; | |||||
bool _power_ready = false; | |||||
bool _power_newdata = false; | |||||
double _power_current = 0; | |||||
double _power_voltage = 0; | |||||
double _power_apparent = 0; | |||||
MedianFilter _filter_current = MedianFilter(POWER_REPORT_BUFFER); | |||||
#if POWER_HAS_ACTIVE | |||||
double _power_active = 0; | |||||
double _power_reactive = 0; | |||||
double _power_factor = 0; | |||||
MedianFilter _filter_voltage = MedianFilter(POWER_REPORT_BUFFER); | |||||
MedianFilter _filter_active = MedianFilter(POWER_REPORT_BUFFER); | |||||
MedianFilter _filter_apparent = MedianFilter(POWER_REPORT_BUFFER); | |||||
#endif | |||||
// ----------------------------------------------------------------------------- | |||||
// PRIVATE METHODS | |||||
// ----------------------------------------------------------------------------- | |||||
#if WEB_SUPPORT | |||||
void _powerAPISetup() { | |||||
apiRegister(MQTT_TOPIC_CURRENT, MQTT_TOPIC_CURRENT, [](char * buffer, size_t len) { | |||||
if (_power_ready) { | |||||
dtostrf(getCurrent(), len-1, POWER_CURRENT_DECIMALS, buffer); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
apiRegister(MQTT_TOPIC_VOLTAGE, MQTT_TOPIC_VOLTAGE, [](char * buffer, size_t len) { | |||||
if (_power_ready) { | |||||
snprintf_P(buffer, len, PSTR("%d"), getVoltage()); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
apiRegister(MQTT_TOPIC_POWER_APPARENT, MQTT_TOPIC_POWER_APPARENT, [](char * buffer, size_t len) { | |||||
if (_power_ready) { | |||||
snprintf_P(buffer, len, PSTR("%d"), getApparentPower()); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
#if POWER_HAS_ACTIVE | |||||
apiRegister(MQTT_TOPIC_POWER_ACTIVE, MQTT_TOPIC_POWER_ACTIVE, [](char * buffer, size_t len) { | |||||
if (_power_ready) { | |||||
snprintf_P(buffer, len, PSTR("%d"), getActivePower()); | |||||
} else { | |||||
buffer = NULL; | |||||
} | |||||
}); | |||||
#endif | |||||
} | |||||
#endif // WEB_SUPPORT | |||||
void _powerReset() { | |||||
_filter_current.reset(); | |||||
#if POWER_HAS_ACTIVE | |||||
_filter_apparent.reset(); | |||||
_filter_voltage.reset(); | |||||
_filter_active.reset(); | |||||
#endif | |||||
} | |||||
void _powerRead() { | |||||
// Get instantaneous values from HAL | |||||
double current = _powerCurrent(); | |||||
double voltage = _powerVoltage(); | |||||
double apparent = _powerApparentPower(); | |||||
#if POWER_HAS_ACTIVE | |||||
double active = _powerActivePower(); | |||||
double reactive = (apparent > active) ? sqrt(apparent * apparent - active * active) : 0; | |||||
double factor = (apparent > 0) ? active / apparent : 1; | |||||
if (factor > 1) factor = 1; | |||||
#endif | |||||
// Filters | |||||
_filter_current.add(current); | |||||
#if POWER_HAS_ACTIVE | |||||
_filter_apparent.add(apparent); | |||||
_filter_voltage.add(voltage); | |||||
_filter_active.add(active); | |||||
#endif | |||||
/* THERE IS A BUG HERE SOMEWHERE :) | |||||
char current_buffer[10]; | |||||
dtostrf(current, sizeof(current_buffer)-1, POWER_CURRENT_DECIMALS, current_buffer); | |||||
DEBUG_MSG_P(PSTR("[POWER] Current: %sA\n"), current_buffer); | |||||
DEBUG_MSG_P(PSTR("[POWER] Voltage: %sA\n"), int(voltage)); | |||||
DEBUG_MSG_P(PSTR("[POWER] Apparent Power: %dW\n"), int(apparent)); | |||||
#if POWER_HAS_ACTIVE | |||||
DEBUG_MSG_P(PSTR("[POWER] Active Power: %dW\n"), int(active)); | |||||
DEBUG_MSG_P(PSTR("[POWER] Reactive Power: %dW\n"), int(reactive)); | |||||
DEBUG_MSG_P(PSTR("[POWER] Power Factor: %d%%\n"), int(100 * factor)); | |||||
#endif | |||||
*/ | |||||
// Update websocket clients | |||||
#if WEB_SUPPORT | |||||
if (wsConnected()) { | |||||
DynamicJsonBuffer jsonBuffer; | |||||
JsonObject& root = jsonBuffer.createObject(); | |||||
root["pwrVisible"] = 1; | |||||
root["pwrCurrent"] = roundTo(current, POWER_CURRENT_DECIMALS); | |||||
root["pwrVoltage"] = roundTo(voltage, POWER_VOLTAGE_DECIMALS); | |||||
root["pwrApparent"] = roundTo(apparent, POWER_POWER_DECIMALS); | |||||
#if POWER_HAS_ACTIVE | |||||
root["pwrActive"] = roundTo(active, POWER_POWER_DECIMALS); | |||||
root["pwrReactive"] = roundTo(reactive, POWER_POWER_DECIMALS); | |||||
root["pwrFactor"] = int(100 * factor); | |||||
#endif | |||||
#if (POWER_PROVIDER == POWER_PROVIDER_EMON_ANALOG) || (POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121) | |||||
root["emonVisible"] = 1; | |||||
#endif | |||||
#if POWER_PROVIDER == POWER_PROVIDER_HLW8012 | |||||
root["hlwVisible"] = 1; | |||||
#endif | |||||
#if POWER_PROVIDER == POWER_PROVIDER_V9261F | |||||
root["v9261fVisible"] = 1; | |||||
#endif | |||||
#if POWER_PROVIDER == POWER_PROVIDER_ECH1560 | |||||
root["ech1560Visible"] = 1; | |||||
#endif | |||||
String output; | |||||
root.printTo(output); | |||||
wsSend(output.c_str()); | |||||
} | |||||
#endif | |||||
} | |||||
void _powerReport() { | |||||
// Get the fitered values | |||||
_power_current = _filter_current.average(true); | |||||
#if POWER_HAS_ACTIVE | |||||
_power_apparent = _filter_apparent.average(true); | |||||
_power_voltage = _filter_voltage.average(true); | |||||
_power_active = _filter_active.average(true); | |||||
if (_power_active > _power_apparent) _power_apparent = _power_active; | |||||
_power_reactive = (_power_apparent > _power_active) ? sqrt(_power_apparent * _power_apparent - _power_active * _power_active) : 0; | |||||
_power_factor = (_power_apparent > 0) ? _power_active / _power_apparent : 1; | |||||
if (_power_factor > 1) _power_factor = 1; | |||||
double power = _power_active; | |||||
#else | |||||
_power_apparent = _power_current * _power_voltage; | |||||
double power = _power_apparent; | |||||
#endif | |||||
double energy_delta = power * POWER_ENERGY_FACTOR; | |||||
_power_ready = true; | |||||
char buf_current[10]; | |||||
char buf_energy[10]; | |||||
dtostrf(_power_current, 1-sizeof(buf_current), POWER_CURRENT_DECIMALS, buf_current); | |||||
dtostrf(energy_delta, 1-sizeof(buf_energy), POWER_CURRENT_DECIMALS, buf_energy); | |||||
{ | |||||
mqttSend(MQTT_TOPIC_CURRENT, buf_current); | |||||
mqttSend(MQTT_TOPIC_POWER_APPARENT, String((int) _power_apparent).c_str()); | |||||
mqttSend(MQTT_TOPIC_ENERGY, buf_energy); | |||||
#if POWER_HAS_ACTIVE | |||||
mqttSend(MQTT_TOPIC_POWER_ACTIVE, String((int) _power_active).c_str()); | |||||
mqttSend(MQTT_TOPIC_POWER_REACTIVE, String((int) _power_reactive).c_str()); | |||||
mqttSend(MQTT_TOPIC_VOLTAGE, String((int) _power_voltage).c_str()); | |||||
mqttSend(MQTT_TOPIC_POWER_FACTOR, String((int) 100 * _power_factor).c_str()); | |||||
#endif | |||||
} | |||||
#if DOMOTICZ_SUPPORT | |||||
if (domoticzEnabled()) { | |||||
char buffer[20]; | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%d;%s"), power, buf_energy); | |||||
domoticzSend("dczPowIdx", 0, buffer); | |||||
domoticzSend("dczCurrentIdx", 0, buf_current); | |||||
domoticzSend("dczEnergyIdx", 0, buf_energy); | |||||
#if POWER_HAS_ACTIVE | |||||
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _power_voltage); | |||||
domoticzSend("dczVoltIdx", 0, buffer); | |||||
#endif | |||||
} | |||||
#endif | |||||
#if INFLUXDB_SUPPORT | |||||
if (influxdbEnabled()) { | |||||
influxDBSend(MQTT_TOPIC_CURRENT, buf_current); | |||||
influxDBSend(MQTT_TOPIC_POWER_APPARENT, String((int) _power_apparent).c_str()); | |||||
influxDBSend(MQTT_TOPIC_ENERGY, buf_energy); | |||||
#if POWER_HAS_ACTIVE | |||||
influxDBSend(MQTT_TOPIC_POWER_ACTIVE, String((int) _power_active).c_str()); | |||||
influxDBSend(MQTT_TOPIC_POWER_REACTIVE, String((int) _power_reactive).c_str()); | |||||
influxDBSend(MQTT_TOPIC_VOLTAGE, String((int) _power_voltage).c_str()); | |||||
influxDBSend(MQTT_TOPIC_POWER_FACTOR, String((int) 100 * _power_factor).c_str()); | |||||
#endif | |||||
} | |||||
#endif | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// MAGNITUDE API | |||||
// ----------------------------------------------------------------------------- | |||||
bool hasActivePower() { | |||||
return POWER_HAS_ACTIVE; | |||||
} | |||||
double getCurrent() { | |||||
return roundTo(_power_current, POWER_CURRENT_DECIMALS); | |||||
} | |||||
double getVoltage() { | |||||
return roundTo(_power_voltage, POWER_VOLTAGE_DECIMALS); | |||||
} | |||||
double getApparentPower() { | |||||
return roundTo(_power_apparent, POWER_POWER_DECIMALS); | |||||
} | |||||
#if POWER_HAS_ACTIVE | |||||
double getActivePower() { | |||||
return roundTo(_power_active, POWER_POWER_DECIMALS); | |||||
} | |||||
double getReactivePower() { | |||||
return roundTo(_power_reactive, POWER_POWER_DECIMALS); | |||||
} | |||||
double getPowerFactor() { | |||||
return roundTo(_power_factor, 2); | |||||
} | |||||
#endif | |||||
// ----------------------------------------------------------------------------- | |||||
// PUBLIC API | |||||
// ----------------------------------------------------------------------------- | |||||
bool powerEnabled() { | |||||
return _power_enabled; | |||||
} | |||||
void powerEnabled(bool enabled) { | |||||
if (enabled & !_power_enabled) _powerReset(); | |||||
_power_enabled = enabled; | |||||
_powerEnabledProvider(); | |||||
} | |||||
void powerCalibrate(unsigned char magnitude, double value) { | |||||
_powerCalibrateProvider(magnitude, value); | |||||
} | |||||
void powerResetCalibration() { | |||||
_powerResetCalibrationProvider(); | |||||
} | |||||
void powerConfigure() { | |||||
_powerConfigureProvider(); | |||||
} | |||||
void powerSetup() { | |||||
// backwards compatibility | |||||
moveSetting("pwMainsVoltage", "pwrVoltage"); | |||||
moveSetting("emonMains", "pwrVoltage"); | |||||
moveSetting("emonVoltage", "pwrVoltage"); | |||||
moveSetting("pwCurrentRatio", "pwrRatioC"); | |||||
moveSetting("emonRatio", "pwrRatioC"); | |||||
moveSetting("powPowerMult", "pwrRatioP"); | |||||
moveSetting("powCurrentMult", "pwrRatioC"); | |||||
moveSetting("powVoltageMult", "pwrRatioV"); | |||||
moveSetting("powerVoltage", "pwrVoltage"); | |||||
moveSetting("powerRatioC", "pwrRatioC"); | |||||
moveSetting("powerRatioV", "pwrRatioV"); | |||||
moveSetting("powerRatioP", "pwrRatioP"); | |||||
_powerSetupProvider(); | |||||
// API | |||||
#if WEB_SUPPORT | |||||
_powerAPISetup(); | |||||
#endif | |||||
DEBUG_MSG_P(PSTR("[POWER] POWER_PROVIDER = %d\n"), POWER_PROVIDER); | |||||
} | |||||
void powerLoop() { | |||||
_powerLoopProvider(true); | |||||
if (_power_newdata) { | |||||
_power_newdata = false; | |||||
_powerRead(); | |||||
} | |||||
static unsigned long last = 0; | |||||
if (millis() - last > POWER_REPORT_INTERVAL) { | |||||
last = millis(); | |||||
_powerReport(); | |||||
} | |||||
_powerLoopProvider(false); | |||||
} | |||||
#endif // POWER_PROVIDER != POWER_PROVIDER_NONE |
@ -0,0 +1,207 @@ | |||||
/* | |||||
POWER ECH1560 MODULE | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if POWER_PROVIDER == POWER_PROVIDER_ECH1560 | |||||
// ----------------------------------------------------------------------------- | |||||
// MODULE GLOBALS AND CACHE | |||||
// ----------------------------------------------------------------------------- | |||||
volatile long _ech1560_bits_count = 0; | |||||
volatile long _ech1560_clk_count = 0; | |||||
volatile bool _ech1560_dosync = false; | |||||
volatile bool _ech1560_nextbit = true; | |||||
double _ech1560_apparent = 0; | |||||
double _ech1560_voltage = 0; | |||||
double _ech1560_current = 0; | |||||
// ----------------------------------------------------------------------------- | |||||
// HAL | |||||
// ----------------------------------------------------------------------------- | |||||
void _ech1560_sync() { | |||||
unsigned int byte1 = 0; | |||||
unsigned int byte2 = 0; | |||||
unsigned int byte3 = 0; | |||||
_ech1560_bits_count = 0; | |||||
while (_ech1560_bits_count < 40); // skip the uninteresting 5 first bytes | |||||
_ech1560_bits_count = 0; | |||||
while (_ech1560_bits_count < 24) { // loop through the next 3 Bytes (6-8) and save byte 6 and 7 in Ba and Bb | |||||
if (_ech1560_nextbit) { | |||||
if (_ech1560_bits_count < 9) { // first Byte/8 bits in Ba | |||||
byte1 = byte1 << 1; | |||||
if (digitalRead(ECH1560_MISO_PIN) == HIGH) byte1 |= 1; | |||||
_ech1560_nextbit = false; | |||||
} else if (_ech1560_bits_count < 17) { // bit 9-16 is byte 7, stor in Bb | |||||
byte2 = byte2 << 1; | |||||
if (digitalRead(ECH1560_MISO_PIN) == HIGH) byte2 |= 1; | |||||
_ech1560_nextbit = false; | |||||
} | |||||
} | |||||
} | |||||
if (byte2 != 3) { // if bit Bb is not 3, we have reached the important part, U is allready in Ba and Bb and next 8 Bytes will give us the Power. | |||||
// voltage = 2 * (Ba + Bb / 255) | |||||
_ech1560_voltage = 2.0 * ((float) byte1 + (float) byte2 / 255.0); | |||||
// power: | |||||
_ech1560_bits_count = 0; | |||||
while (_ech1560_bits_count < 40); // skip the uninteresting 5 first bytes | |||||
_ech1560_bits_count = 0; | |||||
byte1 = 0; | |||||
byte2 = 0; | |||||
byte3 = 0; | |||||
while (_ech1560_bits_count < 24) { //store byte 6, 7 and 8 in Ba and Bb & Bc. | |||||
if (_ech1560_nextbit) { | |||||
if (_ech1560_bits_count < 9) { | |||||
byte1 = byte1 << 1; | |||||
if (digitalRead(ECH1560_MISO_PIN) == HIGH) byte1 |= 1; | |||||
_ech1560_nextbit = false; | |||||
} else if (_ech1560_bits_count < 17) { | |||||
byte2 = byte2 << 1; | |||||
if (digitalRead(ECH1560_MISO_PIN) == HIGH) byte2 |= 1; | |||||
_ech1560_nextbit = false; | |||||
} else { | |||||
byte3 = byte3 << 1; | |||||
if (digitalRead(ECH1560_MISO_PIN) == HIGH) byte3 |= 1; | |||||
_ech1560_nextbit = false; | |||||
} | |||||
} | |||||
} | |||||
#if ECH1560_INVERTED | |||||
byte1 = 255 - byte1; | |||||
byte2 = 255 - byte2; | |||||
byte3 = 255 - byte3; | |||||
#endif | |||||
// power = (Ba*255+Bb+Bc/255)/2 | |||||
_ech1560_apparent = ( (float) byte1 * 255 + (float) byte2 + (float) byte3 / 255.0) / 2; | |||||
_ech1560_current = _ech1560_apparent / _ech1560_voltage; | |||||
_power_newdata = true; | |||||
_ech1560_dosync = false; | |||||
} | |||||
// If Bb is not 3 or something else than 0, something is wrong! | |||||
if (byte2 == 0) _ech1560_dosync = false; | |||||
} | |||||
void ICACHE_RAM_ATTR _ech1560_isr() { | |||||
// if we are trying to find the sync-time (CLK goes high for 1-2ms) | |||||
if (_ech1560_dosync == false) { | |||||
_ech1560_clk_count = 0; | |||||
// register how long the ClkHigh is high to evaluate if we are at the part wher clk goes high for 1-2 ms | |||||
while (digitalRead(ECH1560_CLK_PIN) == HIGH) { | |||||
_ech1560_clk_count += 1; | |||||
delayMicroseconds(30); //can only use delayMicroseconds in an interrupt. | |||||
} | |||||
// if the Clk was high between 1 and 2 ms than, its a start of a SPI-transmission | |||||
if (_ech1560_clk_count >= 33 && _ech1560_clk_count <= 67) { | |||||
_ech1560_dosync = true; | |||||
} | |||||
// we are in sync and logging CLK-highs | |||||
} else { | |||||
// increment an integer to keep track of how many bits we have read. | |||||
_ech1560_bits_count += 1; | |||||
_ech1560_nextbit = true; | |||||
} | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// POWER API | |||||
// ----------------------------------------------------------------------------- | |||||
double _powerCurrent() { | |||||
return _ech1560_current; | |||||
} | |||||
double _powerVoltage() { | |||||
return _ech1560_voltage; | |||||
} | |||||
double _powerActivePower() { | |||||
return 0; | |||||
} | |||||
double _powerApparentPower() { | |||||
return _ech1560_apparent; | |||||
} | |||||
double _powerReactivePower() { | |||||
return 0; | |||||
} | |||||
double _powerPowerFactor() { | |||||
return 1; | |||||
} | |||||
void _powerEnabledProvider() { | |||||
// Nothing to do | |||||
} | |||||
void _powerConfigureProvider() { | |||||
// Nothing to do | |||||
} | |||||
void _powerCalibrateProvider(unsigned char magnitude, double value) { | |||||
// Nothing to do | |||||
} | |||||
void _powerResetCalibrationProvider() { | |||||
// Nothing to do | |||||
} | |||||
void _powerSetupProvider() { | |||||
pinMode(ECH1560_CLK_PIN, INPUT); | |||||
pinMode(ECH1560_MISO_PIN, INPUT); | |||||
attachInterrupt(ECH1560_CLK_PIN, _ech1560_isr, RISING); | |||||
} | |||||
void _powerLoopProvider(bool before) { | |||||
if (!before) { | |||||
if (_ech1560_dosync) _ech1560_sync(); | |||||
} | |||||
} | |||||
#endif // POWER_PROVIDER == POWER_PROVIDER_ECH1560 |
@ -0,0 +1,164 @@ | |||||
/* | |||||
POWER EMON MODULE | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if (POWER_PROVIDER == POWER_PROVIDER_EMON_ANALOG) || (POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121) | |||||
// ----------------------------------------------------------------------------- | |||||
// MODULE GLOBALS AND CACHE | |||||
// ----------------------------------------------------------------------------- | |||||
#include <EmonLiteESP.h> | |||||
EmonLiteESP _emon; | |||||
#if POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121 | |||||
#include "brzo_i2c.h" | |||||
// ADC121 Registers | |||||
#define ADC121_REG_RESULT 0x00 | |||||
#define ADC121_REG_ALERT 0x01 | |||||
#define ADC121_REG_CONFIG 0x02 | |||||
#define ADC121_REG_LIMITL 0x03 | |||||
#define ADC121_REG_LIMITH 0x04 | |||||
#define ADC121_REG_HYST 0x05 | |||||
#define ADC121_REG_CONVL 0x06 | |||||
#define ADC121_REG_CONVH 0x07 | |||||
#endif // POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121 | |||||
// ----------------------------------------------------------------------------- | |||||
// HAL | |||||
// ----------------------------------------------------------------------------- | |||||
unsigned int currentCallback() { | |||||
#if POWER_PROVIDER == POWER_PROVIDER_EMON_ANALOG | |||||
return analogRead(A0); | |||||
#endif // POWER_PROVIDER == POWER_PROVIDER_EMON_ANALOG | |||||
#if POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121 | |||||
uint8_t buffer[2]; | |||||
brzo_i2c_start_transaction(ADC121_I2C_ADDRESS, I2C_SCL_FREQUENCY); | |||||
buffer[0] = ADC121_REG_RESULT; | |||||
brzo_i2c_write(buffer, 1, false); | |||||
brzo_i2c_read(buffer, 2, false); | |||||
brzo_i2c_end_transaction(); | |||||
unsigned int value; | |||||
value = (buffer[0] & 0x0F) << 8; | |||||
value |= buffer[1]; | |||||
return value; | |||||
#endif // POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121 | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// POWER API | |||||
// ----------------------------------------------------------------------------- | |||||
double _powerCurrent() { | |||||
static unsigned long last = 0; | |||||
static double current = 0; | |||||
if (millis() - last > 1000) { | |||||
last = millis(); | |||||
current = _emon.getCurrent(EMON_SAMPLES); | |||||
current -= EMON_CURRENT_OFFSET; | |||||
if (current < 0) current = 0; | |||||
} | |||||
return current; | |||||
} | |||||
double _powerVoltage() { | |||||
return _power_voltage; | |||||
} | |||||
double _powerActivePower() { | |||||
return 0; | |||||
} | |||||
double _powerApparentPower() { | |||||
return _powerCurrent() * _powerVoltage(); | |||||
} | |||||
double _powerReactivePower() { | |||||
return 0; | |||||
} | |||||
double _powerPowerFactor() { | |||||
return 1; | |||||
} | |||||
void _powerEnabledProvider() { | |||||
// Nothing to do | |||||
} | |||||
void _powerCalibrateProvider(unsigned char magnitude, double value) { | |||||
if (value <= 0) return; | |||||
if (magnitude == POWER_MAGNITUDE_ACTIVE) { | |||||
double power = _powerApparentPower(); | |||||
double ratio = getSetting("pwrRatioC", EMON_CURRENT_RATIO).toFloat(); | |||||
ratio = ratio * (value / power); | |||||
_emon.setCurrentRatio(ratio); | |||||
setSetting("pwrRatioC", ratio); | |||||
saveSettings(); | |||||
} | |||||
if (magnitude == POWER_MAGNITUDE_VOLTAGE) { | |||||
_power_voltage = value; | |||||
setSetting("pwrVoltage", value); | |||||
saveSettings(); | |||||
} | |||||
} | |||||
void _powerResetCalibrationProvider() { | |||||
delSetting("pwrRatioC"); | |||||
_powerConfigureProvider(); | |||||
saveSettings(); | |||||
} | |||||
void _powerConfigureProvider() { | |||||
_emon.setCurrentRatio(getSetting("pwrRatioC", EMON_CURRENT_RATIO).toFloat()); | |||||
_power_voltage = getSetting("pwrVoltage", POWER_VOLTAGE).toFloat(); | |||||
} | |||||
void _powerSetupProvider() { | |||||
_emon.initCurrent(currentCallback, EMON_ADC_BITS, EMON_REFERENCE_VOLTAGE, EMON_CURRENT_RATIO); | |||||
#if POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121 | |||||
uint8_t buffer[2]; | |||||
buffer[0] = ADC121_REG_CONFIG; | |||||
buffer[1] = 0x00; | |||||
brzo_i2c_start_transaction(ADC121_I2C_ADDRESS, I2C_SCL_FREQUENCY); | |||||
brzo_i2c_write(buffer, 2, false); | |||||
brzo_i2c_end_transaction(); | |||||
#endif | |||||
_powerConfigureProvider(); | |||||
_emon.warmup(); | |||||
} | |||||
void _powerLoopProvider(bool before) { | |||||
if (before) { | |||||
static unsigned long last = 0; | |||||
if (millis() - last > POWER_READ_INTERVAL) { | |||||
last = millis(); | |||||
_power_newdata = true; | |||||
} | |||||
} | |||||
} | |||||
#endif // (POWER_PROVIDER == POWER_PROVIDER_EMON_ANALOG) || (POWER_PROVIDER == POWER_PROVIDER_EMON_ADC121) |
@ -0,0 +1,161 @@ | |||||
/* | |||||
POWER HLW8012 MODULE | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if POWER_PROVIDER == POWER_PROVIDER_HLW8012 | |||||
// ----------------------------------------------------------------------------- | |||||
// MODULE GLOBALS AND CACHE | |||||
// ----------------------------------------------------------------------------- | |||||
#include <HLW8012.h> | |||||
#include <ESP8266WiFi.h> | |||||
HLW8012 _hlw8012; | |||||
WiFiEventHandler _power_wifi_onconnect; | |||||
WiFiEventHandler _power_wifi_ondisconnect; | |||||
// ----------------------------------------------------------------------------- | |||||
// HAL | |||||
// ----------------------------------------------------------------------------- | |||||
void ICACHE_RAM_ATTR _hlw_cf1_isr() { | |||||
_hlw8012.cf1_interrupt(); | |||||
} | |||||
void ICACHE_RAM_ATTR _hlw_cf_isr() { | |||||
_hlw8012.cf_interrupt(); | |||||
} | |||||
void _hlwRestoreCalibration() { | |||||
double value; | |||||
value = getSetting("pwrRatioP", 0).toFloat(); | |||||
if (value > 0) _hlw8012.setPowerMultiplier(value); | |||||
value = getSetting("pwrRatioC", 0).toFloat(); | |||||
if (value > 0) _hlw8012.setCurrentMultiplier(value); | |||||
value = getSetting("pwrRatioV", 0).toFloat(); | |||||
if (value > 0) _hlw8012.setVoltageMultiplier(value); | |||||
} | |||||
void _hlwPersistCalibration() { | |||||
setSetting("pwrRatioP", _hlw8012.getPowerMultiplier()); | |||||
setSetting("pwrRatioC", _hlw8012.getCurrentMultiplier()); | |||||
setSetting("pwrRatioV", _hlw8012.getVoltageMultiplier()); | |||||
saveSettings(); | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// POWER API | |||||
// ----------------------------------------------------------------------------- | |||||
double _powerCurrent() { | |||||
return _hlw8012.getCurrent(); | |||||
} | |||||
double _powerVoltage() { | |||||
return _hlw8012.getVoltage(); | |||||
} | |||||
double _powerActivePower() { | |||||
return _hlw8012.getActivePower(); | |||||
} | |||||
double _powerApparentPower() { | |||||
return _hlw8012.getApparentPower(); | |||||
} | |||||
double _powerReactivePower() { | |||||
return _hlw8012.getReactivePower(); | |||||
} | |||||
double _powerPowerFactor() { | |||||
return _hlw8012.getPowerFactor(); | |||||
} | |||||
void _powerEnabledProvider() { | |||||
if (_power_enabled) { | |||||
attachInterrupt(HLW8012_CF1_PIN, _hlw_cf1_isr, CHANGE); | |||||
attachInterrupt(HLW8012_CF_PIN, _hlw_cf_isr, CHANGE); | |||||
} else { | |||||
detachInterrupt(HLW8012_CF1_PIN); | |||||
detachInterrupt(HLW8012_CF_PIN); | |||||
} | |||||
} | |||||
void _powerCalibrateProvider(unsigned char magnitude, double value) { | |||||
if (value <= 0) return; | |||||
if (magnitude == POWER_MAGNITUDE_ACTIVE) _hlw8012.expectedActivePower(value); | |||||
if (magnitude == POWER_MAGNITUDE_CURRENT) _hlw8012.expectedCurrent(value); | |||||
if (magnitude == POWER_MAGNITUDE_VOLTAGE) _hlw8012.expectedVoltage(value); | |||||
_hlwPersistCalibration(); | |||||
} | |||||
void _powerResetCalibrationProvider() { | |||||
_hlw8012.resetMultipliers(); | |||||
delSetting("pwrRatioC"); | |||||
delSetting("pwrRatioV"); | |||||
delSetting("pwrRatioP"); | |||||
saveSettings(); | |||||
} | |||||
void _powerConfigureProvider() { | |||||
// Nothing to do | |||||
} | |||||
void _powerSetupProvider() { | |||||
// 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 | |||||
#if HLW8012_USE_INTERRUPTS | |||||
_hlw8012.begin(HLW8012_CF_PIN, HLW8012_CF1_PIN, HLW8012_SEL_PIN, HLW8012_SEL_CURRENT, true); | |||||
#else | |||||
_hlw8012.begin(HLW8012_CF_PIN, HLW8012_CF1_PIN, HLW8012_SEL_PIN, HLW8012_SEL_CURRENT, false, 1000000); | |||||
#endif | |||||
// 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(HLW8012_CURRENT_R, HLW8012_VOLTAGE_R_UP, HLW8012_VOLTAGE_R_DOWN); | |||||
_hlwRestoreCalibration(); | |||||
_power_wifi_onconnect = WiFi.onStationModeGotIP([](WiFiEventStationModeGotIP ipInfo) { | |||||
powerEnabled(true); | |||||
}); | |||||
_power_wifi_ondisconnect = WiFi.onStationModeDisconnected([](WiFiEventStationModeDisconnected ipInfo) { | |||||
powerEnabled(false); | |||||
}); | |||||
} | |||||
void _powerLoopProvider(bool before) { | |||||
if (before) { | |||||
static unsigned long last = 0; | |||||
if (millis() - last > POWER_READ_INTERVAL) { | |||||
last = millis(); | |||||
_power_newdata = true; | |||||
// Toggle between current and voltage monitoring | |||||
#if (HLW8012_USE_INTERRUPTS == 0) | |||||
_hlw8012.toggleMode(); | |||||
#endif // (HLW8012_USE_INTERRUPTS == 0) | |||||
} | |||||
} | |||||
} | |||||
#endif // POWER_PROVIDER == POWER_PROVIDER_HLW8012 |
@ -0,0 +1,234 @@ | |||||
/* | |||||
POWER V9261F MODULE | |||||
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com> | |||||
*/ | |||||
#if POWER_PROVIDER == POWER_PROVIDER_V9261F | |||||
// ----------------------------------------------------------------------------- | |||||
// MODULE GLOBALS AND CACHE | |||||
// ----------------------------------------------------------------------------- | |||||
#include <SoftwareSerial.h> | |||||
SoftwareSerial * _v9261f_uart; | |||||
double _v9261f_active = 0; | |||||
double _v9261f_reactive = 0; | |||||
double _v9261f_voltage = 0; | |||||
double _v9261f_current = 0; | |||||
double _v9261f_ratioP = V9261F_POWER_FACTOR; | |||||
double _v9261f_ratioC = V9261F_CURRENT_FACTOR; | |||||
double _v9261f_ratioV = V9261F_VOLTAGE_FACTOR; | |||||
double _v9261f_ratioR = V9261F_RPOWER_FACTOR; | |||||
unsigned char _v9261f_data[24]; | |||||
// ----------------------------------------------------------------------------- | |||||
// HAL | |||||
// ----------------------------------------------------------------------------- | |||||
void _v9261fRead() { | |||||
static unsigned char state = 0; | |||||
static unsigned long last = 0; | |||||
static bool found = false; | |||||
static unsigned char index = 0; | |||||
if (state == 0) { | |||||
while (_v9261f_uart->available()) { | |||||
_v9261f_uart->flush(); | |||||
found = true; | |||||
last = millis(); | |||||
} | |||||
if (found && (millis() - last > V9261F_SYNC_INTERVAL)) { | |||||
_v9261f_uart->flush(); | |||||
index = 0; | |||||
state = 1; | |||||
} | |||||
} else if (state == 1) { | |||||
while (_v9261f_uart->available()) { | |||||
_v9261f_uart->read(); | |||||
if (index++ >= 7) { | |||||
_v9261f_uart->flush(); | |||||
index = 0; | |||||
state = 2; | |||||
} | |||||
} | |||||
} else if (state == 2) { | |||||
while (_v9261f_uart->available()) { | |||||
_v9261f_data[index] = _v9261f_uart->read(); | |||||
if (index++ >= 19) { | |||||
_v9261f_uart->flush(); | |||||
last = millis(); | |||||
state = 3; | |||||
} | |||||
} | |||||
} else if (state == 3) { | |||||
if (_v9261fChecksum()) { | |||||
_v9261f_active = (double) ( | |||||
(_v9261f_data[3]) + | |||||
(_v9261f_data[4] << 8) + | |||||
(_v9261f_data[5] << 16) + | |||||
(_v9261f_data[6] << 24) | |||||
) / _v9261f_ratioP; | |||||
_v9261f_reactive = (double) ( | |||||
(_v9261f_data[7]) + | |||||
(_v9261f_data[8] << 8) + | |||||
(_v9261f_data[9] << 16) + | |||||
(_v9261f_data[10] << 24) | |||||
) / _v9261f_ratioR; | |||||
_v9261f_voltage = (double) ( | |||||
(_v9261f_data[11]) + | |||||
(_v9261f_data[12] << 8) + | |||||
(_v9261f_data[13] << 16) + | |||||
(_v9261f_data[14] << 24) | |||||
) / _v9261f_ratioV; | |||||
_v9261f_current = (double) ( | |||||
(_v9261f_data[15]) + | |||||
(_v9261f_data[16] << 8) + | |||||
(_v9261f_data[17] << 16) + | |||||
(_v9261f_data[18] << 24) | |||||
) / _v9261f_ratioC; | |||||
if (_v9261f_active < 0) _v9261f_active = 0; | |||||
if (_v9261f_reactive < 0) _v9261f_reactive = 0; | |||||
if (_v9261f_voltage < 0) _v9261f_voltage = 0; | |||||
if (_v9261f_current < 0) _v9261f_current = 0; | |||||
_power_newdata = true; | |||||
} | |||||
last = millis(); | |||||
index = 0; | |||||
state = 4; | |||||
} else if (state == 4) { | |||||
while (_v9261f_uart->available()) { | |||||
_v9261f_uart->flush(); | |||||
last = millis(); | |||||
} | |||||
if (millis() - last > V9261F_SYNC_INTERVAL) { | |||||
state = 1; | |||||
} | |||||
} | |||||
} | |||||
bool _v9261fChecksum() { | |||||
unsigned char checksum = 0; | |||||
for (unsigned char i = 0; i < 19; i++) { | |||||
checksum = checksum + _v9261f_data[i]; | |||||
} | |||||
checksum = ~checksum + 0x33; | |||||
return checksum == _v9261f_data[19]; | |||||
} | |||||
// ----------------------------------------------------------------------------- | |||||
// POWER API | |||||
// ----------------------------------------------------------------------------- | |||||
double _powerCurrent() { | |||||
return _v9261f_current; | |||||
} | |||||
double _powerVoltage() { | |||||
return _v9261f_voltage; | |||||
} | |||||
double _powerActivePower() { | |||||
return _v9261f_active; | |||||
} | |||||
double _powerApparentPower() { | |||||
return sqrt(_v9261f_reactive * _v9261f_reactive + _v9261f_active * _v9261f_active); | |||||
} | |||||
double _powerReactivePower() { | |||||
return _v9261f_reactive; | |||||
} | |||||
double _powerPowerFactor() { | |||||
double apparent = _powerApparentPower(); | |||||
if (apparent > 0) return _powerActivePower() / apparent; | |||||
return 1; | |||||
} | |||||
void _powerEnabledProvider() { | |||||
// Nothing to do | |||||
} | |||||
void _powerConfigureProvider() { | |||||
_v9261f_ratioP = getSetting("pwrRatioP", V9261F_POWER_FACTOR).toFloat(); | |||||
_v9261f_ratioV = getSetting("pwrRatioV", V9261F_VOLTAGE_FACTOR).toFloat(); | |||||
_v9261f_ratioC = getSetting("pwrRatioC", V9261F_CURRENT_FACTOR).toFloat(); | |||||
_v9261f_ratioR = getSetting("pwrRatioR", V9261F_RPOWER_FACTOR).toFloat(); | |||||
} | |||||
void _powerCalibrateProvider(unsigned char magnitude, double value) { | |||||
if (value <= 0) return; | |||||
if (magnitude == POWER_MAGNITUDE_ACTIVE) { | |||||
_v9261f_ratioP = _v9261f_ratioP * (_v9261f_active / value); | |||||
setSetting("pwrRatioP", _v9261f_ratioP); | |||||
} | |||||
if (magnitude == POWER_MAGNITUDE_CURRENT) { | |||||
_v9261f_ratioC = _v9261f_ratioC * (_v9261f_current / value); | |||||
setSetting("pwrRatioC", _v9261f_ratioC); | |||||
} | |||||
if (magnitude == POWER_MAGNITUDE_VOLTAGE) { | |||||
_v9261f_ratioV = _v9261f_ratioV * (_v9261f_voltage / value); | |||||
setSetting("pwrRatioV", _v9261f_ratioV); | |||||
} | |||||
if (magnitude == POWER_MAGNITUDE_POWER_FACTOR) { | |||||
if (value < 100) { | |||||
double apparent = _v9261f_ratioP / (value / 100); | |||||
value = sqrt(apparent * apparent - _v9261f_ratioP * _v9261f_ratioP); | |||||
_v9261f_ratioR = _v9261f_ratioR * (_v9261f_reactive / value); | |||||
setSetting("pwrRatioR", _v9261f_ratioR); | |||||
} | |||||
} | |||||
saveSettings(); | |||||
} | |||||
void _powerResetCalibrationProvider() { | |||||
delSetting("pwrRatioP"); | |||||
delSetting("pwrRatioC"); | |||||
delSetting("pwrRatioV"); | |||||
delSetting("pwrRatioR"); | |||||
_powerConfigureProvider(); | |||||
saveSettings(); | |||||
} | |||||
void _powerSetupProvider() { | |||||
_v9261f_uart = new SoftwareSerial(V9261F_PIN, SW_SERIAL_UNUSED_PIN, V9261F_PIN_INVERSE, 256); | |||||
_v9261f_uart->begin(V9261F_BAUDRATE); | |||||
} | |||||
void _powerLoopProvider(bool before) { | |||||
if (before) { | |||||
_v9261fRead(); | |||||
} | |||||
} | |||||
#endif // POWER_PROVIDER == POWER_PROVIDER_V9261F |