/* 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 . */ #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 { uint32_t hlw_len; 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); 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; 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; 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) { if (XdrvMailbox.data_len && Hlw.cf_power_pulse_length ) { XdrvMailbox.payload = ((uint32_t)(CharToFloat(XdrvMailbox.data) * 10) * Hlw.cf_power_pulse_length ) / Hlw.power_ratio; } } else if (CMND_VOLTAGESET == Energy->command_code) { if (XdrvMailbox.data_len && Hlw.cf1_voltage_pulse_length ) { XdrvMailbox.payload = ((uint32_t)(CharToFloat(XdrvMailbox.data) * 10) * Hlw.cf1_voltage_pulse_length ) / Hlw.voltage_ratio; } } else if (CMND_CURRENTSET == Energy->command_code) { if (XdrvMailbox.data_len && Hlw.cf1_current_pulse_length) { XdrvMailbox.payload = ((uint32_t)(CharToFloat(XdrvMailbox.data)) * 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