Energy added
This commit is contained in:
parent
45cb91688c
commit
797ce586aa
|
@ -0,0 +1,316 @@
|
||||||
|
/*
|
||||||
|
xnrg_02_cse7766.ino - CSE7766 and HLW8032 energy sensor support for Tasmota
|
||||||
|
|
||||||
|
Copyright (C) 2021 Theo Arends
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef USE_ENERGY_SENSOR
|
||||||
|
#ifdef USE_CSE7766
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* CSE7759 and CSE7766 - Energy (Sonoff S31 and Sonoff Pow R2/R3)
|
||||||
|
* HLW8032 - Energy (Blitzwolf SHP5)
|
||||||
|
*
|
||||||
|
* Needs GPIO_CSE7766_RX only
|
||||||
|
*
|
||||||
|
* Based on datasheet from http://www.chipsea.com/UploadFiles/2017/08/11144342F01B5662.pdf
|
||||||
|
\*********************************************************************************************/
|
||||||
|
|
||||||
|
#define XNRG_02 2
|
||||||
|
|
||||||
|
#define CSE_MAX_INVALID_POWER 128 // Number of invalid power receipts before deciding active power is zero
|
||||||
|
|
||||||
|
#define CSE_NOT_CALIBRATED 0xAA
|
||||||
|
|
||||||
|
#define CSE_PULSES_NOT_INITIALIZED -1
|
||||||
|
|
||||||
|
#define CSE_PREF 1000
|
||||||
|
#define CSE_UREF 100
|
||||||
|
|
||||||
|
#define CSE_BUFFER_SIZE 25
|
||||||
|
|
||||||
|
#include <TasmotaSerial.h>
|
||||||
|
|
||||||
|
TasmotaSerial *CseSerial = nullptr;
|
||||||
|
|
||||||
|
struct CSE {
|
||||||
|
long voltage_cycle = 0;
|
||||||
|
long current_cycle = 0;
|
||||||
|
long power_cycle = 0;
|
||||||
|
long power_cycle_first = 0;
|
||||||
|
long cf_pulses = 0;
|
||||||
|
long cf_pulses_last_time = CSE_PULSES_NOT_INITIALIZED;
|
||||||
|
|
||||||
|
int byte_counter = 0;
|
||||||
|
uint8_t *rx_buffer = nullptr;
|
||||||
|
uint8_t power_invalid = 0;
|
||||||
|
bool received = false;
|
||||||
|
} Cse;
|
||||||
|
|
||||||
|
void CseReceived(void) {
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
||||||
|
// F2 5A 02 F7 60 00 03 61 00 40 10 05 72 40 51 A6 58 63 10 1B E1 7F 4D 4E - F2 = Power cycle exceeds range - takes too long - No load
|
||||||
|
// 55 5A 02 F7 60 00 03 5A 00 40 10 04 8B 9F 51 A6 58 18 72 75 61 AC A1 30 - 55 = Ok, 61 = Power not valid (load below 5W)
|
||||||
|
// 55 5A 02 F7 60 00 03 AB 00 40 10 02 60 5D 51 A6 58 03 E9 EF 71 0B 7A 36 - 55 = Ok, 71 = Ok, F1 = CF overflow, no problem
|
||||||
|
// 55 5A 02 F1 E8 00 07 9D 00 3F 3E 00 35 F8 50 DB 38 00 B2 A2 F1 D6 97 3E - CF overflow
|
||||||
|
|
||||||
|
// 55 5A 02 DB 40 00 03 25 00 3D 18 03 8E CD 4F 0A 60 2A 56 85 61 01 02 1A - OK voltage
|
||||||
|
// 55 5A 02 DB 40 07 17 1D 00 3D 18 03 8E CD 4F 0A 60 2B EF EA 61 01 02 2C - Wrong voltage
|
||||||
|
// Hd Id VCal---- Voltage- ICal---- Current- PCal---- Power--- Ad CF--- Ck
|
||||||
|
|
||||||
|
uint8_t header = Cse.rx_buffer[0];
|
||||||
|
if ((header & 0xFC) == 0xFC) {
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CSE: Abnormal hardware"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chip calibration data (coefficients) and use as initial defaults
|
||||||
|
if (HLW_UREF_PULSE == EnergyGetCalibration(ENERGY_VOLTAGE_CALIBRATION)) {
|
||||||
|
long voltage_coefficient = 191200; // uSec
|
||||||
|
if (CSE_NOT_CALIBRATED != header) {
|
||||||
|
voltage_coefficient = Cse.rx_buffer[2] << 16 | Cse.rx_buffer[3] << 8 | Cse.rx_buffer[4];
|
||||||
|
}
|
||||||
|
EnergySetCalibration(ENERGY_VOLTAGE_CALIBRATION, voltage_coefficient / CSE_UREF);
|
||||||
|
}
|
||||||
|
if (HLW_IREF_PULSE == EnergyGetCalibration(ENERGY_CURRENT_CALIBRATION)) {
|
||||||
|
long current_coefficient = 16140; // uSec
|
||||||
|
if (CSE_NOT_CALIBRATED != header) {
|
||||||
|
current_coefficient = Cse.rx_buffer[8] << 16 | Cse.rx_buffer[9] << 8 | Cse.rx_buffer[10];
|
||||||
|
}
|
||||||
|
EnergySetCalibration(ENERGY_CURRENT_CALIBRATION, current_coefficient);
|
||||||
|
}
|
||||||
|
if (HLW_PREF_PULSE == EnergyGetCalibration(ENERGY_POWER_CALIBRATION)) {
|
||||||
|
long power_coefficient = 5364000; // uSec
|
||||||
|
if (CSE_NOT_CALIBRATED != header) {
|
||||||
|
power_coefficient = Cse.rx_buffer[14] << 16 | Cse.rx_buffer[15] << 8 | Cse.rx_buffer[16];
|
||||||
|
}
|
||||||
|
EnergySetCalibration(ENERGY_POWER_CALIBRATION, power_coefficient / CSE_PREF);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t adjustement = Cse.rx_buffer[20];
|
||||||
|
Cse.voltage_cycle = Cse.rx_buffer[5] << 16 | Cse.rx_buffer[6] << 8 | Cse.rx_buffer[7];
|
||||||
|
Cse.current_cycle = Cse.rx_buffer[11] << 16 | Cse.rx_buffer[12] << 8 | Cse.rx_buffer[13];
|
||||||
|
Cse.power_cycle = Cse.rx_buffer[17] << 16 | Cse.rx_buffer[18] << 8 | Cse.rx_buffer[19];
|
||||||
|
Cse.cf_pulses = Cse.rx_buffer[21] << 8 | Cse.rx_buffer[22];
|
||||||
|
|
||||||
|
if (Energy->power_on) { // Powered on
|
||||||
|
if (adjustement & 0x40) { // Voltage valid
|
||||||
|
Energy->voltage[0] = (float)(EnergyGetCalibration(ENERGY_VOLTAGE_CALIBRATION) * CSE_UREF) / (float)Cse.voltage_cycle;
|
||||||
|
}
|
||||||
|
if (adjustement & 0x10) { // Power valid
|
||||||
|
Cse.power_invalid = 0;
|
||||||
|
if ((header & 0xF2) == 0xF2) { // Power cycle exceeds range
|
||||||
|
Energy->active_power[0] = 0;
|
||||||
|
} else {
|
||||||
|
if (0 == Cse.power_cycle_first) { Cse.power_cycle_first = Cse.power_cycle; } // Skip first incomplete Cse.power_cycle
|
||||||
|
if (Cse.power_cycle_first != Cse.power_cycle) {
|
||||||
|
Cse.power_cycle_first = -1;
|
||||||
|
Energy->active_power[0] = (float)(EnergyGetCalibration(ENERGY_POWER_CALIBRATION) * CSE_PREF) / (float)Cse.power_cycle;
|
||||||
|
} else {
|
||||||
|
Energy->active_power[0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Cse.power_invalid < Settings->param[P_CSE7766_INVALID_POWER]) { // Allow measurements down to about 1W
|
||||||
|
Cse.power_invalid++;
|
||||||
|
} else {
|
||||||
|
Cse.power_cycle_first = 0;
|
||||||
|
Energy->active_power[0] = 0; // Powered on but no load
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adjustement & 0x20) { // Current valid
|
||||||
|
if (0 == Energy->active_power[0]) {
|
||||||
|
Energy->current[0] = 0;
|
||||||
|
} else {
|
||||||
|
Energy->current[0] = (float)EnergyGetCalibration(ENERGY_CURRENT_CALIBRATION) / (float)Cse.current_cycle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Powered off
|
||||||
|
Cse.power_cycle_first = 0;
|
||||||
|
Energy->voltage[0] = 0;
|
||||||
|
Energy->active_power[0] = 0;
|
||||||
|
Energy->current[0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CseSerialInput(void) {
|
||||||
|
while (CseSerial->available()) {
|
||||||
|
yield();
|
||||||
|
uint8_t serial_in_byte = CseSerial->read();
|
||||||
|
|
||||||
|
if (Cse.received) {
|
||||||
|
Cse.rx_buffer[Cse.byte_counter++] = serial_in_byte;
|
||||||
|
if (24 == Cse.byte_counter) {
|
||||||
|
|
||||||
|
AddLogBuffer(LOG_LEVEL_DEBUG_MORE, Cse.rx_buffer, 24);
|
||||||
|
|
||||||
|
uint8_t checksum = 0;
|
||||||
|
for (uint32_t i = 2; i < 23; i++) { checksum += Cse.rx_buffer[i]; }
|
||||||
|
if (checksum == Cse.rx_buffer[23]) {
|
||||||
|
Energy->data_valid[0] = 0;
|
||||||
|
CseReceived();
|
||||||
|
Cse.received = false;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
do { // Sync buffer with data (issue #1907 and #3425)
|
||||||
|
memmove(Cse.rx_buffer, Cse.rx_buffer +1, 24);
|
||||||
|
Cse.byte_counter--;
|
||||||
|
} while ((Cse.byte_counter > 2) && (0x5A != Cse.rx_buffer[1]));
|
||||||
|
if (0x5A != Cse.rx_buffer[1]) {
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CSE: " D_CHECKSUM_FAILURE));
|
||||||
|
Cse.received = false;
|
||||||
|
Cse.byte_counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ((0x5A == serial_in_byte) && (1 == Cse.byte_counter)) { // 0x5A - Packet header 2
|
||||||
|
Cse.received = true;
|
||||||
|
} else {
|
||||||
|
Cse.byte_counter = 0;
|
||||||
|
}
|
||||||
|
Cse.rx_buffer[Cse.byte_counter++] = serial_in_byte;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************************************************************************/
|
||||||
|
|
||||||
|
void CseEverySecond(void) {
|
||||||
|
if (Energy->data_valid[0] > ENERGY_WATCHDOG) {
|
||||||
|
Cse.voltage_cycle = 0;
|
||||||
|
Cse.current_cycle = 0;
|
||||||
|
Cse.power_cycle = 0;
|
||||||
|
} else {
|
||||||
|
if (CSE_PULSES_NOT_INITIALIZED == Cse.cf_pulses_last_time) {
|
||||||
|
Cse.cf_pulses_last_time = Cse.cf_pulses; // Init after restart
|
||||||
|
} else {
|
||||||
|
uint32_t cf_pulses = 0;
|
||||||
|
if (Cse.cf_pulses < Cse.cf_pulses_last_time) { // Rolled over after 0xFFFF (65535) pulses
|
||||||
|
cf_pulses = (0x10000 - Cse.cf_pulses_last_time) + Cse.cf_pulses;
|
||||||
|
} else {
|
||||||
|
cf_pulses = Cse.cf_pulses - Cse.cf_pulses_last_time;
|
||||||
|
}
|
||||||
|
if (cf_pulses && Energy->active_power[0]) {
|
||||||
|
uint32_t delta = (cf_pulses * EnergyGetCalibration(ENERGY_POWER_CALIBRATION)) / 36;
|
||||||
|
// prevent invalid load delta steps even checksum is valid (issue #5789):
|
||||||
|
// prevent invalid load delta steps even checksum is valid but allow up to 4kW (issue #7155):
|
||||||
|
// if (delta <= (4000 * 1000 / 36)) { // max load for S31/Pow R2: 4.00kW
|
||||||
|
// prevent invalid load delta steps even checksum is valid but allow up to 5.5kW (issue #14156):
|
||||||
|
if (delta <= (5500 * 1000 / 36)) { // max load for Pow R3: 5.50kW
|
||||||
|
Cse.cf_pulses_last_time = Cse.cf_pulses;
|
||||||
|
Energy->kWhtoday_delta[0] += delta;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CSE: Overload"));
|
||||||
|
Cse.cf_pulses_last_time = CSE_PULSES_NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
EnergyUpdateToday();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CseSnsInit(void) {
|
||||||
|
// Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions
|
||||||
|
// CseSerial = new TasmotaSerial(Pin(GPIO_CSE7766_RX), Pin(GPIO_CSE7766_TX), 1);
|
||||||
|
CseSerial = new TasmotaSerial(Pin(GPIO_CSE7766_RX), -1, 1);
|
||||||
|
if (CseSerial->begin(4800, SERIAL_8E1)) {
|
||||||
|
if (CseSerial->hardwareSerial()) {
|
||||||
|
SetSerial(4800, TS_SERIAL_8E1);
|
||||||
|
ClaimSerial();
|
||||||
|
}
|
||||||
|
#ifdef ESP32
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("CSE: Serial UART%d"), CseSerial->getUart());
|
||||||
|
#endif
|
||||||
|
if (0 == Settings->param[P_CSE7766_INVALID_POWER]) {
|
||||||
|
Settings->param[P_CSE7766_INVALID_POWER] = CSE_MAX_INVALID_POWER; // SetOption39 1..255
|
||||||
|
}
|
||||||
|
Cse.power_invalid = Settings->param[P_CSE7766_INVALID_POWER];
|
||||||
|
Energy->use_overtemp = true; // Use global temperature for overtemp detection
|
||||||
|
} else {
|
||||||
|
TasmotaGlobal.energy_driver = ENERGY_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CseDrvInit(void) {
|
||||||
|
// if (PinUsed(GPIO_CSE7766_RX) && PinUsed(GPIO_CSE7766_TX)) {
|
||||||
|
if (PinUsed(GPIO_CSE7766_RX)) {
|
||||||
|
Cse.rx_buffer = (uint8_t*)(malloc(CSE_BUFFER_SIZE));
|
||||||
|
if (Cse.rx_buffer != nullptr) {
|
||||||
|
TasmotaGlobal.energy_driver = XNRG_02;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CseCommand(void) {
|
||||||
|
bool serviced = true;
|
||||||
|
|
||||||
|
float value = CharToFloat(XdrvMailbox.data);
|
||||||
|
|
||||||
|
if ((CMND_POWERCAL == Energy->command_code) ||
|
||||||
|
(CMND_VOLTAGECAL == Energy->command_code) ||
|
||||||
|
(CMND_CURRENTCAL == Energy->command_code)) {
|
||||||
|
// Service in xdrv_03_energy.ino
|
||||||
|
}
|
||||||
|
else if (CMND_POWERSET == Energy->command_code) { // xxx W
|
||||||
|
if (XdrvMailbox.data_len && Cse.power_cycle) {
|
||||||
|
XdrvMailbox.payload = (uint32_t)(value * Cse.power_cycle) / CSE_PREF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CMND_VOLTAGESET == Energy->command_code) { // xxx V
|
||||||
|
if (XdrvMailbox.data_len && Cse.voltage_cycle) {
|
||||||
|
XdrvMailbox.payload = (uint32_t)(value * Cse.voltage_cycle) / CSE_UREF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CMND_CURRENTSET == Energy->command_code) { // xxx mA
|
||||||
|
if (XdrvMailbox.data_len && Cse.current_cycle) {
|
||||||
|
XdrvMailbox.payload = (uint32_t)(value * Cse.current_cycle) / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else serviced = false; // Unknown command
|
||||||
|
|
||||||
|
return serviced;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* Interface
|
||||||
|
\*********************************************************************************************/
|
||||||
|
|
||||||
|
bool Xnrg02(uint32_t function) {
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
switch (function) {
|
||||||
|
case FUNC_LOOP:
|
||||||
|
if (CseSerial) { CseSerialInput(); }
|
||||||
|
break;
|
||||||
|
case FUNC_EVERY_SECOND:
|
||||||
|
CseEverySecond();
|
||||||
|
break;
|
||||||
|
case FUNC_COMMAND:
|
||||||
|
result = CseCommand();
|
||||||
|
break;
|
||||||
|
case FUNC_INIT:
|
||||||
|
CseSnsInit();
|
||||||
|
break;
|
||||||
|
case FUNC_PRE_INIT:
|
||||||
|
CseDrvInit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // USE_CSE7766
|
||||||
|
#endif // USE_ENERGY_SENSOR
|
|
@ -0,0 +1,40 @@
|
||||||
|
#pragma once
|
||||||
|
#ifdef USE_ENERGY_SENSOR
|
||||||
|
#ifdef USE_CSE7766
|
||||||
|
|
||||||
|
#define XNRG_02 2
|
||||||
|
|
||||||
|
#define CSE_MAX_INVALID_POWER 128 // Number of invalid power readings before deciding active power is zero
|
||||||
|
|
||||||
|
#define CSE_NOT_CALIBRATED 0xAA
|
||||||
|
|
||||||
|
#define CSE_PULSES_NOT_INITIALIZED -1
|
||||||
|
|
||||||
|
#define CSE_PREF 1000
|
||||||
|
#define CSE_UREF 100
|
||||||
|
|
||||||
|
#define CSE_BUFFER_SIZE 25
|
||||||
|
|
||||||
|
struct CSE {
|
||||||
|
volatile uint32_t cf_pulse_length = 0;
|
||||||
|
volatile uint32_t cf_pulse_last_time = 0;
|
||||||
|
volatile uint32_t cf_summed_pulse_length = 0;
|
||||||
|
volatile uint32_t cf_pulse_counter = 0;
|
||||||
|
uint32_t cf_power_pulse_length = 0;
|
||||||
|
|
||||||
|
int byte_counter = 0;
|
||||||
|
uint8_t *rx_buffer = nullptr;
|
||||||
|
uint8_t power_invalid = 0;
|
||||||
|
bool received = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void CseEvery200ms(void);
|
||||||
|
void CseEverySecond(void);
|
||||||
|
void CseSnsInit(void);
|
||||||
|
void CseDrvInit(void);
|
||||||
|
bool CseCommand(void);
|
||||||
|
|
||||||
|
bool Xnrg02(uint32_t function);
|
||||||
|
|
||||||
|
#endif // USE_CSE7766
|
||||||
|
#endif // USE_ENERGY_SENSOR
|
|
@ -0,0 +1,342 @@
|
||||||
|
/*
|
||||||
|
xnrg_01_hlw8012.ino - HLW8012 (Sonoff Pow) energy sensor support for Tasmota
|
||||||
|
|
||||||
|
Copyright (C) 2021 Theo Arends
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef USE_ENERGY_SENSOR
|
||||||
|
#ifdef USE_HLW8012
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* HLW8012, BL0937 or HJL-01 - Energy (Sonoff Pow, HuaFan, KMC70011, BlitzWolf)
|
||||||
|
*
|
||||||
|
* Based on Source: Shenzhen Heli Technology Co., Ltd
|
||||||
|
\*********************************************************************************************/
|
||||||
|
|
||||||
|
#define XNRG_01 1
|
||||||
|
|
||||||
|
// Energy model type 0 (GPIO_HLW_CF) - HLW8012 based (Sonoff Pow, KMC70011, HuaFan, AplicWDP303075)
|
||||||
|
#define HLW_PREF 10000 // 1000.0W
|
||||||
|
#define HLW_UREF 2200 // 220.0V
|
||||||
|
#define HLW_IREF 4545 // 4.545A
|
||||||
|
|
||||||
|
// Energy model type 1 (GPIO_HJL_CF) - HJL-01/BL0937 based (BlitzWolf, Homecube, Gosund, Teckin)
|
||||||
|
#define HJL_PREF 1362
|
||||||
|
#define HJL_UREF 822
|
||||||
|
#define HJL_IREF 3300
|
||||||
|
|
||||||
|
#define HLW_POWER_PROBE_TIME 10 // Number of seconds to probe for power before deciding none used (low power pulse can take up to 10 seconds)
|
||||||
|
#define HLW_SAMPLE_COUNT 10 // Max number of samples per cycle
|
||||||
|
|
||||||
|
//#define HLW_DEBUG
|
||||||
|
|
||||||
|
struct HLW {
|
||||||
|
#ifdef HLW_DEBUG
|
||||||
|
uint32_t debug[HLW_SAMPLE_COUNT];
|
||||||
|
#endif
|
||||||
|
volatile uint32_t cf_pulse_length = 0;
|
||||||
|
volatile uint32_t cf_pulse_last_time = 0;
|
||||||
|
volatile uint32_t cf_summed_pulse_length = 0;
|
||||||
|
volatile uint32_t cf_pulse_counter = 0;
|
||||||
|
uint32_t cf_power_pulse_length = 0;
|
||||||
|
|
||||||
|
volatile uint32_t cf1_pulse_length = 0;
|
||||||
|
volatile uint32_t cf1_pulse_last_time = 0;
|
||||||
|
volatile uint32_t cf1_summed_pulse_length = 0;
|
||||||
|
volatile uint32_t cf1_pulse_counter = 0;
|
||||||
|
|
||||||
|
uint32_t cf1_voltage_pulse_length = 0;
|
||||||
|
uint32_t cf1_current_pulse_length = 0;
|
||||||
|
|
||||||
|
volatile uint32_t energy_period_counter = 0;
|
||||||
|
|
||||||
|
uint32_t power_ratio = 0;
|
||||||
|
uint32_t voltage_ratio = 0;
|
||||||
|
uint32_t current_ratio = 0;
|
||||||
|
|
||||||
|
uint8_t model_type = 0;
|
||||||
|
volatile uint8_t cf1_timer = 0;
|
||||||
|
uint8_t power_retry = 0;
|
||||||
|
bool select_ui_flag = false;
|
||||||
|
bool ui_flag = true;
|
||||||
|
volatile bool load_off = true;
|
||||||
|
} Hlw;
|
||||||
|
|
||||||
|
// Fix core 2.5.x ISR not in IRAM Exception
|
||||||
|
#ifndef USE_WS2812_DMA // Collides with Neopixelbus but solves exception
|
||||||
|
void HlwCfInterrupt(void) IRAM_ATTR;
|
||||||
|
void HlwCf1Interrupt(void) IRAM_ATTR;
|
||||||
|
#endif // USE_WS2812_DMA
|
||||||
|
|
||||||
|
void HlwCfInterrupt(void) { // Service Power
|
||||||
|
uint32_t us = micros();
|
||||||
|
|
||||||
|
if (Hlw.load_off) { // Restart plen measurement
|
||||||
|
Hlw.cf_pulse_last_time = us;
|
||||||
|
Hlw.load_off = false;
|
||||||
|
} else {
|
||||||
|
Hlw.cf_pulse_length = us - Hlw.cf_pulse_last_time;
|
||||||
|
Hlw.cf_pulse_last_time = us;
|
||||||
|
Hlw.cf_summed_pulse_length += Hlw.cf_pulse_length;
|
||||||
|
Hlw.cf_pulse_counter++;
|
||||||
|
Hlw.energy_period_counter++;
|
||||||
|
}
|
||||||
|
Energy->data_valid[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HlwCf1Interrupt(void) { // Service Voltage and Current
|
||||||
|
uint32_t us = micros();
|
||||||
|
|
||||||
|
Hlw.cf1_pulse_length = us - Hlw.cf1_pulse_last_time;
|
||||||
|
Hlw.cf1_pulse_last_time = us;
|
||||||
|
if ((Hlw.cf1_timer > 2) && (Hlw.cf1_timer < 8)) { // Allow for 300 mSec set-up time and measure for up to 1 second
|
||||||
|
Hlw.cf1_summed_pulse_length += Hlw.cf1_pulse_length;
|
||||||
|
#ifdef HLW_DEBUG
|
||||||
|
Hlw.debug[Hlw.cf1_pulse_counter] = Hlw.cf1_pulse_length;
|
||||||
|
#endif
|
||||||
|
Hlw.cf1_pulse_counter++;
|
||||||
|
if (HLW_SAMPLE_COUNT == Hlw.cf1_pulse_counter) {
|
||||||
|
Hlw.cf1_timer = 8; // We need up to HLW_SAMPLE_COUNT samples within 1 second (low current could take up to 0.3 second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Energy->data_valid[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************************************************************************/
|
||||||
|
|
||||||
|
void HlwEvery200ms(void) {
|
||||||
|
uint32_t cf1_pulse_length = 0;
|
||||||
|
uint32_t hlw_w = 0;
|
||||||
|
uint32_t hlw_u = 0;
|
||||||
|
uint32_t hlw_i = 0;
|
||||||
|
|
||||||
|
if (micros() - Hlw.cf_pulse_last_time > (HLW_POWER_PROBE_TIME * 1000000)) {
|
||||||
|
Hlw.cf_pulse_length = 0; // No load for some time
|
||||||
|
Hlw.load_off = true;
|
||||||
|
}
|
||||||
|
Hlw.cf_power_pulse_length = Hlw.cf_pulse_length;
|
||||||
|
if (Hlw.cf_pulse_counter && !Hlw.load_off) {
|
||||||
|
Hlw.cf_power_pulse_length = Hlw.cf_summed_pulse_length / Hlw.cf_pulse_counter;
|
||||||
|
}
|
||||||
|
Hlw.cf_summed_pulse_length = 0;
|
||||||
|
Hlw.cf_pulse_counter = 0;
|
||||||
|
|
||||||
|
if (Hlw.cf_power_pulse_length && Energy->power_on && !Hlw.load_off) {
|
||||||
|
hlw_w = (Hlw.power_ratio * EnergyGetCalibration(ENERGY_POWER_CALIBRATION)) / Hlw.cf_power_pulse_length ; // W *10
|
||||||
|
Energy->active_power[0] = (float)hlw_w / 10;
|
||||||
|
Hlw.power_retry = 1; // Workaround issue #5161
|
||||||
|
} else {
|
||||||
|
if (Hlw.power_retry) {
|
||||||
|
Hlw.power_retry--;
|
||||||
|
} else {
|
||||||
|
Energy->active_power[0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinUsed(GPIO_NRG_CF1)) {
|
||||||
|
Hlw.cf1_timer++;
|
||||||
|
if (Hlw.cf1_timer >= 8) {
|
||||||
|
Hlw.cf1_timer = 0;
|
||||||
|
Hlw.select_ui_flag = (Hlw.select_ui_flag) ? false : true;
|
||||||
|
DigitalWrite(GPIO_NRG_SEL, 0, Hlw.select_ui_flag);
|
||||||
|
|
||||||
|
if (Hlw.cf1_pulse_counter) {
|
||||||
|
cf1_pulse_length = Hlw.cf1_summed_pulse_length / Hlw.cf1_pulse_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HLW_DEBUG
|
||||||
|
// Debugging for calculating mean and median
|
||||||
|
char stemp[100];
|
||||||
|
stemp[0] = '\0';
|
||||||
|
for (uint32_t i = 0; i < Hlw.cf1_pulse_counter; i++) {
|
||||||
|
snprintf_P(stemp, sizeof(stemp), PSTR("%s %d"), stemp, Hlw.debug[i]);
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < Hlw.cf1_pulse_counter; i++) {
|
||||||
|
for (uint32_t j = i + 1; j < Hlw.cf1_pulse_counter; j++) {
|
||||||
|
if (Hlw.debug[i] > Hlw.debug[j]) { // Sort ascending
|
||||||
|
std::swap(Hlw.debug[i], Hlw.debug[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t median = Hlw.debug[(Hlw.cf1_pulse_counter +1) / 2];
|
||||||
|
AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: power %d, ui %d, cnt %d, smpl%s, sum %d, mean %d, median %d"),
|
||||||
|
Hlw.cf_power_pulse_length , Hlw.select_ui_flag, Hlw.cf1_pulse_counter, stemp, Hlw.cf1_summed_pulse_length, cf1_pulse_length, median);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (Hlw.select_ui_flag == Hlw.ui_flag) {
|
||||||
|
Hlw.cf1_voltage_pulse_length = cf1_pulse_length;
|
||||||
|
|
||||||
|
if (Hlw.cf1_voltage_pulse_length && Energy->power_on) { // If powered on always provide voltage
|
||||||
|
hlw_u = (Hlw.voltage_ratio * EnergyGetCalibration(ENERGY_VOLTAGE_CALIBRATION)) / Hlw.cf1_voltage_pulse_length ; // V *10
|
||||||
|
Energy->voltage[0] = (float)hlw_u / 10;
|
||||||
|
} else {
|
||||||
|
Energy->voltage[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Hlw.cf1_current_pulse_length = cf1_pulse_length;
|
||||||
|
|
||||||
|
if (Hlw.cf1_current_pulse_length && Energy->active_power[0]) { // No current if no power being consumed
|
||||||
|
hlw_i = (Hlw.current_ratio * EnergyGetCalibration(ENERGY_CURRENT_CALIBRATION)) / Hlw.cf1_current_pulse_length; // mA
|
||||||
|
Energy->current[0] = (float)hlw_i / 1000;
|
||||||
|
} else {
|
||||||
|
Energy->current[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Hlw.cf1_summed_pulse_length = 0;
|
||||||
|
Hlw.cf1_pulse_counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HlwEverySecond(void) {
|
||||||
|
if (Energy->data_valid[0] > ENERGY_WATCHDOG) {
|
||||||
|
Hlw.cf1_voltage_pulse_length = 0;
|
||||||
|
Hlw.cf1_current_pulse_length = 0;
|
||||||
|
Hlw.cf_power_pulse_length = 0;
|
||||||
|
} else {
|
||||||
|
if (Hlw.energy_period_counter) {
|
||||||
|
|
||||||
|
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("HLW: EPC %u, CFlen %d usec"), Hlw.energy_period_counter, Hlw.cf_pulse_length);
|
||||||
|
|
||||||
|
uint32_t hlw_len = 10000 * 100 / Hlw.energy_period_counter; // Add *100 to fix rounding on loads at 3.6kW (#9160)
|
||||||
|
Hlw.energy_period_counter = 0;
|
||||||
|
if (hlw_len) {
|
||||||
|
Energy->kWhtoday_delta[0] += (((Hlw.power_ratio * EnergyGetCalibration(ENERGY_POWER_CALIBRATION)) / 36) * 100) / hlw_len;
|
||||||
|
// Energy->kWhtoday_delta[0] += Energy->active_power[0] * 1000 / 36;
|
||||||
|
EnergyUpdateToday();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HlwSnsInit(void) {
|
||||||
|
if (!EnergyGetCalibration(ENERGY_POWER_CALIBRATION) || (4975 == EnergyGetCalibration(ENERGY_POWER_CALIBRATION))) {
|
||||||
|
EnergySetCalibration(ENERGY_POWER_CALIBRATION, HLW_PREF_PULSE);
|
||||||
|
EnergySetCalibration(ENERGY_VOLTAGE_CALIBRATION, HLW_UREF_PULSE);
|
||||||
|
EnergySetCalibration(ENERGY_CURRENT_CALIBRATION, HLW_IREF_PULSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hlw.model_type) {
|
||||||
|
Hlw.power_ratio = HJL_PREF;
|
||||||
|
Hlw.voltage_ratio = HJL_UREF;
|
||||||
|
Hlw.current_ratio = HJL_IREF;
|
||||||
|
} else {
|
||||||
|
Hlw.power_ratio = HLW_PREF;
|
||||||
|
Hlw.voltage_ratio = HLW_UREF;
|
||||||
|
Hlw.current_ratio = HLW_IREF;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinUsed(GPIO_NRG_SEL)) {
|
||||||
|
pinMode(Pin(GPIO_NRG_SEL), OUTPUT);
|
||||||
|
digitalWrite(Pin(GPIO_NRG_SEL), Hlw.select_ui_flag);
|
||||||
|
}
|
||||||
|
if (PinUsed(GPIO_NRG_CF1)) {
|
||||||
|
pinMode(Pin(GPIO_NRG_CF1), INPUT_PULLUP);
|
||||||
|
attachInterrupt(Pin(GPIO_NRG_CF1), HlwCf1Interrupt, FALLING);
|
||||||
|
}
|
||||||
|
pinMode(Pin(GPIO_HLW_CF), INPUT_PULLUP);
|
||||||
|
attachInterrupt(Pin(GPIO_HLW_CF), HlwCfInterrupt, FALLING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HlwDrvInit(void) {
|
||||||
|
Hlw.model_type = 0; // HLW8012
|
||||||
|
if (PinUsed(GPIO_HJL_CF)) {
|
||||||
|
SetPin(Pin(GPIO_HJL_CF), AGPIO(GPIO_HLW_CF));
|
||||||
|
Hlw.model_type = 1; // HJL-01/BL0937
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinUsed(GPIO_HLW_CF)) { // HLW8012 or HJL-01 based device Power monitor
|
||||||
|
|
||||||
|
Hlw.ui_flag = true; // Voltage on high
|
||||||
|
if (PinUsed(GPIO_NRG_SEL_INV)) {
|
||||||
|
SetPin(Pin(GPIO_NRG_SEL_INV), AGPIO(GPIO_NRG_SEL));
|
||||||
|
Hlw.ui_flag = false; // Voltage on low
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinUsed(GPIO_NRG_CF1)) { // Voltage and/or Current monitor
|
||||||
|
if (!PinUsed(GPIO_NRG_SEL)) { // Voltage and/or Current selector
|
||||||
|
Energy->current_available = false; // Assume Voltage
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Energy->current_available = false;
|
||||||
|
Energy->voltage_available = false;
|
||||||
|
}
|
||||||
|
Energy->use_overtemp = true; // Use global temperature for overtemp detection
|
||||||
|
|
||||||
|
TasmotaGlobal.energy_driver = XNRG_01;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HlwCommand(void) {
|
||||||
|
bool serviced = true;
|
||||||
|
|
||||||
|
float value = CharToFloat(XdrvMailbox.data);
|
||||||
|
|
||||||
|
if ((CMND_POWERCAL == Energy->command_code) ||
|
||||||
|
(CMND_VOLTAGECAL == Energy->command_code) ||
|
||||||
|
(CMND_CURRENTCAL == Energy->command_code)) {
|
||||||
|
// Service in xdrv_03_energy.ino
|
||||||
|
}
|
||||||
|
else if (CMND_POWERSET == Energy->command_code) { // xxx.x W
|
||||||
|
if (XdrvMailbox.data_len && Hlw.cf_power_pulse_length ) {
|
||||||
|
XdrvMailbox.payload = ((uint32_t)(value * 10) * Hlw.cf_power_pulse_length ) / Hlw.power_ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CMND_VOLTAGESET == Energy->command_code) { // xxx.x V
|
||||||
|
if (XdrvMailbox.data_len && Hlw.cf1_voltage_pulse_length ) {
|
||||||
|
XdrvMailbox.payload = ((uint32_t)(value * 10) * Hlw.cf1_voltage_pulse_length ) / Hlw.voltage_ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CMND_CURRENTSET == Energy->command_code) { // xxx mA
|
||||||
|
if (XdrvMailbox.data_len && Hlw.cf1_current_pulse_length) {
|
||||||
|
XdrvMailbox.payload = ((uint32_t)(value) * Hlw.cf1_current_pulse_length) / Hlw.current_ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else serviced = false; // Unknown command
|
||||||
|
|
||||||
|
return serviced;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* Interface
|
||||||
|
\*********************************************************************************************/
|
||||||
|
|
||||||
|
bool Xnrg01(uint32_t function) {
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
switch (function) {
|
||||||
|
case FUNC_EVERY_200_MSECOND:
|
||||||
|
HlwEvery200ms();
|
||||||
|
break;
|
||||||
|
case FUNC_ENERGY_EVERY_SECOND:
|
||||||
|
HlwEverySecond();
|
||||||
|
break;
|
||||||
|
case FUNC_COMMAND:
|
||||||
|
result = HlwCommand();
|
||||||
|
break;
|
||||||
|
case FUNC_INIT:
|
||||||
|
HlwSnsInit();
|
||||||
|
break;
|
||||||
|
case FUNC_PRE_INIT:
|
||||||
|
HlwDrvInit();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // USE_HLW8012
|
||||||
|
#endif // USE_ENERGY_SENSOR
|
|
@ -0,0 +1,64 @@
|
||||||
|
#pragma once
|
||||||
|
#ifdef USE_ENERGY_SENSOR
|
||||||
|
#ifdef USE_HLW8012
|
||||||
|
|
||||||
|
#define XNRG_01 1
|
||||||
|
|
||||||
|
// Energy model type 0 (GPIO_HLW_CF) - HLW8012 based (Sonoff Pow, KMC70011, HuaFan, AplicWDP303075)
|
||||||
|
#define HLW_PREF 10000 // 1000.0W
|
||||||
|
#define HLW_UREF 2200 // 220.0V
|
||||||
|
#define HLW_IREF 4545 // 4.545A
|
||||||
|
|
||||||
|
// Energy model type 1 (GPIO_HJL_CF) - HJL-01/BL0937 based (BlitzWolf, Homecube, Gosund, Teckin)
|
||||||
|
#define HJL_PREF 1362
|
||||||
|
#define HJL_UREF 822
|
||||||
|
#define HJL_IREF 3300
|
||||||
|
|
||||||
|
#define HLW_POWER_PROBE_TIME 10 // Number of seconds to probe for power before deciding none used
|
||||||
|
#define HLW_SAMPLE_COUNT 10 // Max number of samples per cycle
|
||||||
|
|
||||||
|
struct HLW {
|
||||||
|
volatile uint32_t cf_pulse_length = 0;
|
||||||
|
volatile uint32_t cf_pulse_last_time = 0;
|
||||||
|
volatile uint32_t cf_summed_pulse_length = 0;
|
||||||
|
volatile uint32_t cf_pulse_counter = 0;
|
||||||
|
uint32_t cf_power_pulse_length = 0;
|
||||||
|
|
||||||
|
volatile uint32_t cf1_pulse_length = 0;
|
||||||
|
volatile uint32_t cf1_pulse_last_time = 0;
|
||||||
|
volatile uint32_t cf1_summed_pulse_length = 0;
|
||||||
|
volatile uint32_t cf1_pulse_counter = 0;
|
||||||
|
uint32_t cf1_voltage_pulse_length = 0;
|
||||||
|
uint32_t cf1_current_pulse_length = 0;
|
||||||
|
|
||||||
|
volatile uint32_t energy_period_counter = 0;
|
||||||
|
|
||||||
|
uint32_t power_ratio = 0;
|
||||||
|
uint32_t voltage_ratio = 0;
|
||||||
|
uint32_t current_ratio = 0;
|
||||||
|
|
||||||
|
uint8_t model_type = 0;
|
||||||
|
volatile uint8_t cf1_timer = 0;
|
||||||
|
uint8_t power_retry = 0;
|
||||||
|
bool select_ui_flag = false;
|
||||||
|
bool ui_flag = true;
|
||||||
|
volatile bool load_off = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifndef USE_WS2812_DMA // Collides with Neopixelbus but solves exception
|
||||||
|
void HlwCfInterrupt(void) IRAM_ATTR;
|
||||||
|
void HlwCf1Interrupt(void) IRAM_ATTR;
|
||||||
|
#endif // USE_WS2812_DMA
|
||||||
|
|
||||||
|
void HlwCfInterrupt(void);
|
||||||
|
void HlwCf1Interrupt(void);
|
||||||
|
void HlwEvery200ms(void);
|
||||||
|
void HlwEverySecond(void);
|
||||||
|
void HlwSnsInit(void);
|
||||||
|
void HlwDrvInit(void);
|
||||||
|
bool HlwCommand(void);
|
||||||
|
|
||||||
|
bool Xnrg01(uint32_t function);
|
||||||
|
|
||||||
|
#endif // USE_HLW8012
|
||||||
|
#endif // USE_ENERGY_SENSOR
|
|
@ -0,0 +1,168 @@
|
||||||
|
#include "EnergyMonitor.h"
|
||||||
|
#include <AsyncWiFiSettings.h>
|
||||||
|
#include <AsyncMqttClient.h>
|
||||||
|
#include "defaults.h"
|
||||||
|
#include "globals.h"
|
||||||
|
#include "mqtt.h"
|
||||||
|
|
||||||
|
#define USE_ENERGY_SENSOR 1
|
||||||
|
#define USE_HLW8012 1
|
||||||
|
#define USE_CSE7766 1
|
||||||
|
#include "hlw8012.h"
|
||||||
|
#include "cse7766.h"
|
||||||
|
|
||||||
|
namespace EnergyMonitor {
|
||||||
|
|
||||||
|
const device_config deviceConfigs[] = {
|
||||||
|
// DEVICE_NONE
|
||||||
|
{
|
||||||
|
"None", // name
|
||||||
|
0, // sel_pin
|
||||||
|
false, // sel_inverted
|
||||||
|
0, // cf_pin
|
||||||
|
0, // cf1_pin
|
||||||
|
0, // voltage_divider
|
||||||
|
0, // current_multiplier
|
||||||
|
0, // power_multiplier
|
||||||
|
false // use_cse7766
|
||||||
|
},
|
||||||
|
// DEVICE_SWITCHBOT_PLUG
|
||||||
|
{
|
||||||
|
"SwitchBot Plug Mini", // name
|
||||||
|
20, // sel_pin
|
||||||
|
true, // sel_inverted
|
||||||
|
18, // cf_pin
|
||||||
|
19, // cf1_pin
|
||||||
|
1467, // voltage_divider
|
||||||
|
1, // current_multiplier
|
||||||
|
1, // power_multiplier
|
||||||
|
false // use_cse7766
|
||||||
|
},
|
||||||
|
// DEVICE_ATHOM_PLUG
|
||||||
|
{
|
||||||
|
"Athom Smart Plug", // name
|
||||||
|
0, // sel_pin
|
||||||
|
false, // sel_inverted
|
||||||
|
3, // cf_pin (RX)
|
||||||
|
1, // cf1_pin (TX)
|
||||||
|
0, // voltage_divider (internal)
|
||||||
|
0, // current_multiplier (internal)
|
||||||
|
0, // power_multiplier (internal)
|
||||||
|
true // use_cse7766
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int deviceType = 0;
|
||||||
|
float power = 0;
|
||||||
|
float voltage = 0;
|
||||||
|
float current = 0;
|
||||||
|
float energy = 0;
|
||||||
|
float apparentPower = 0;
|
||||||
|
float reactivePower = 0;
|
||||||
|
float powerFactor = 0;
|
||||||
|
|
||||||
|
void ConnectToWifi() {
|
||||||
|
AsyncWiFiSettings.heading("<a href='https://espresense.com/configuration/settings#energy' target='_blank'>Power Monitoring</a>", false);
|
||||||
|
|
||||||
|
std::vector<String> deviceTypes;
|
||||||
|
for(int i = 0; i < DEVICE_COUNT; i++) {
|
||||||
|
deviceTypes.push_back(deviceConfigs[i].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceType = AsyncWiFiSettings.dropdown("energy_device", deviceTypes, 0, "Power Monitoring Device");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Setup() {
|
||||||
|
if (deviceType == DEVICE_NONE) return;
|
||||||
|
|
||||||
|
const device_config& config = deviceConfigs[deviceType];
|
||||||
|
|
||||||
|
if (!config.use_cse7766) {
|
||||||
|
// HLW8012/BL0937 setup
|
||||||
|
if (config.sel_pin > 0) {
|
||||||
|
pinMode(config.sel_pin, OUTPUT);
|
||||||
|
digitalWrite(config.sel_pin, config.sel_inverted);
|
||||||
|
}
|
||||||
|
if (config.cf_pin > 0) {
|
||||||
|
pinMode(config.cf_pin, INPUT_PULLUP);
|
||||||
|
}
|
||||||
|
if (config.cf1_pin > 0) {
|
||||||
|
pinMode(config.cf1_pin, INPUT_PULLUP);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// CSE7766 setup
|
||||||
|
Serial1.begin(4800, SERIAL_8E1, config.cf_pin, config.cf1_pin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Loop() {
|
||||||
|
if (deviceType == DEVICE_NONE) return;
|
||||||
|
|
||||||
|
const device_config& config = deviceConfigs[deviceType];
|
||||||
|
|
||||||
|
if (!config.use_cse7766) {
|
||||||
|
// HLW8012/BL0937 readings
|
||||||
|
// TODO: Implement readings using interrupts
|
||||||
|
} else {
|
||||||
|
// CSE7766 readings
|
||||||
|
while (Serial1.available()) {
|
||||||
|
// TODO: Implement CSE7766 protocol parsing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SerialReport() {
|
||||||
|
if (deviceType == DEVICE_NONE) return;
|
||||||
|
Serial.printf("Power Monitor: %s\n", deviceConfigs[deviceType].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SendDiscovery() {
|
||||||
|
if (deviceType == DEVICE_NONE) return true;
|
||||||
|
|
||||||
|
bool success = sendSensorDiscovery("Power", EC_NONE, "power", "W") &&
|
||||||
|
sendSensorDiscovery("Voltage", EC_NONE, "voltage", "V") &&
|
||||||
|
sendSensorDiscovery("Current", EC_NONE, "current", "A") &&
|
||||||
|
sendSensorDiscovery("Energy", EC_NONE, "energy", "kWh");
|
||||||
|
|
||||||
|
if (deviceConfigs[deviceType].use_cse7766) {
|
||||||
|
success = success &&
|
||||||
|
sendSensorDiscovery("Apparent Power", EC_NONE, "apparent_power", "VA") &&
|
||||||
|
sendSensorDiscovery("Reactive Power", EC_NONE, "reactive_power", "VAR") &&
|
||||||
|
sendSensorDiscovery("Power Factor", EC_NONE, "power_factor", "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SendOnline() {
|
||||||
|
if (deviceType == DEVICE_NONE) return true;
|
||||||
|
|
||||||
|
bool success = pub((roomsTopic + "/power").c_str(), 0, true, String(power).c_str()) &&
|
||||||
|
pub((roomsTopic + "/voltage").c_str(), 0, true, String(voltage).c_str()) &&
|
||||||
|
pub((roomsTopic + "/current").c_str(), 0, true, String(current).c_str()) &&
|
||||||
|
pub((roomsTopic + "/energy").c_str(), 0, true, String(energy).c_str());
|
||||||
|
|
||||||
|
if (deviceConfigs[deviceType].use_cse7766) {
|
||||||
|
success = success &&
|
||||||
|
pub((roomsTopic + "/apparent_power").c_str(), 0, true, String(apparentPower).c_str()) &&
|
||||||
|
pub((roomsTopic + "/reactive_power").c_str(), 0, true, String(reactivePower).c_str()) &&
|
||||||
|
pub((roomsTopic + "/power_factor").c_str(), 0, true, String(powerFactor).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Command(String& command, String& pay) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float getPower() { return power; }
|
||||||
|
float getVoltage() { return voltage; }
|
||||||
|
float getCurrent() { return current; }
|
||||||
|
float getEnergy() { return energy; }
|
||||||
|
float getApparentPower() { return apparentPower; }
|
||||||
|
float getReactivePower() { return reactivePower; }
|
||||||
|
float getPowerFactor() { return powerFactor; }
|
||||||
|
bool isConnected() { return deviceType != DEVICE_NONE; }
|
||||||
|
|
||||||
|
} // namespace EnergyMonitor
|
|
@ -0,0 +1,45 @@
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace EnergyMonitor {
|
||||||
|
enum DeviceType {
|
||||||
|
DEVICE_NONE = 0,
|
||||||
|
DEVICE_SWITCHBOT_PLUG = 1,
|
||||||
|
DEVICE_ATHOM_PLUG = 2,
|
||||||
|
// Add more devices as needed
|
||||||
|
DEVICE_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
struct device_config {
|
||||||
|
const char* name; // Device marketing name
|
||||||
|
uint8_t sel_pin; // SEL pin for HLW8012/BL0937
|
||||||
|
bool sel_inverted; // SEL pin inverted?
|
||||||
|
uint8_t cf_pin; // CF pin (power)
|
||||||
|
uint8_t cf1_pin; // CF1 pin (voltage/current)
|
||||||
|
int voltage_divider; // Voltage divider value
|
||||||
|
int current_multiplier; // Current multiplier
|
||||||
|
int power_multiplier; // Power multiplier
|
||||||
|
bool use_cse7766; // true = CSE7766, false = HLW8012/BL0937
|
||||||
|
};
|
||||||
|
|
||||||
|
extern const device_config deviceConfigs[];
|
||||||
|
|
||||||
|
void ConnectToWifi();
|
||||||
|
void Setup();
|
||||||
|
void Loop();
|
||||||
|
void SerialReport();
|
||||||
|
bool SendDiscovery();
|
||||||
|
bool SendOnline();
|
||||||
|
bool Command(String& command, String& pay);
|
||||||
|
|
||||||
|
// Sensor readings
|
||||||
|
float getPower();
|
||||||
|
float getVoltage();
|
||||||
|
float getCurrent();
|
||||||
|
float getEnergy();
|
||||||
|
float getApparentPower();
|
||||||
|
float getReactivePower();
|
||||||
|
float getPowerFactor();
|
||||||
|
|
||||||
|
bool isConnected();
|
||||||
|
}
|
3947
src/ui_index_js.h
3947
src/ui_index_js.h
File diff suppressed because it is too large
Load Diff
|
@ -50,6 +50,16 @@
|
||||||
<p><label>Rssi adjustment for receiver (use only if you know this device has a weak antenna):<br /><input type="number" step="1" min="-100" max="100" name="rx_adj_rssi" placeholder="0" bind:value={$extras.rx_adj_rssi}/></label></p>
|
<p><label>Rssi adjustment for receiver (use only if you know this device has a weak antenna):<br /><input type="number" step="1" min="-100" max="100" name="rx_adj_rssi" placeholder="0" bind:value={$extras.rx_adj_rssi}/></label></p>
|
||||||
<p><label>Factor used to account for absorption, reflection, or diffraction:<br /><input type="number" step="0.01" min="-100" max="100" name="absorption" placeholder="3.50" bind:value={$extras.absorption}/></label></p>
|
<p><label>Factor used to account for absorption, reflection, or diffraction:<br /><input type="number" step="0.01" min="-100" max="100" name="absorption" placeholder="3.50" bind:value={$extras.absorption}/></label></p>
|
||||||
<p><label>Rssi expected from this tx power at 1m (used for node iBeacon):<br /><input type="number" step="1" min="-100" max="100" name="tx_ref_rssi" placeholder="-59" bind:value={$extras.tx_ref_rssi}/></label></p>
|
<p><label>Rssi expected from this tx power at 1m (used for node iBeacon):<br /><input type="number" step="1" min="-100" max="100" name="tx_ref_rssi" placeholder="-59" bind:value={$extras.tx_ref_rssi}/></label></p>
|
||||||
|
|
||||||
|
<h2><a href='https://espresense.com/configuration/settings#energy' target='_blank'>Power Monitoring</a></h2>
|
||||||
|
<p><label>Energy Monitoring Device:<br />
|
||||||
|
<select name="energy_device" bind:value={$extras.energy_device}>
|
||||||
|
<option value="0">None</option>
|
||||||
|
<option value="1">SwitchBot Plug</option>
|
||||||
|
<option value="2">Anthom Smart Plug</option>
|
||||||
|
</select>
|
||||||
|
</label></p>
|
||||||
|
|
||||||
<h2><a href="https://espresense.com/configuration/settings#leds" target="_blank">LEDs</a></h2>
|
<h2><a href="https://espresense.com/configuration/settings#leds" target="_blank">LEDs</a></h2>
|
||||||
<h4>LED 1:</h4>
|
<h4>LED 1:</h4>
|
||||||
<p><label>LED Type:<br /><select name="led_1_type" bind:value={$extras.led_1_type}><option disabled selected hidden>PWM</option><option value="0">PWM</option><option value="1">PWM Inverted</option><option value="2">Addressable GRB</option><option value="3">Addressable GRBW</option><option value="4">Addressable RGB</option><option value="5">Addressable RGBW</option></select></label></p>
|
<p><label>LED Type:<br /><select name="led_1_type" bind:value={$extras.led_1_type}><option disabled selected hidden>PWM</option><option value="0">PWM</option><option value="1">PWM Inverted</option><option value="2">Addressable GRB</option><option value="3">Addressable GRBW</option><option value="4">Addressable RGB</option><option value="5">Addressable RGBW</option></select></label></p>
|
||||||
|
|
Loading…
Reference in New Issue