Mirror of espurna firmware for wireless switches and more
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

3698 lines
109 KiB

/*
SENSOR MODULE
Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#include "espurna.h"
#if SENSOR_SUPPORT
#include "sensor.h"
#include "api.h"
#include "domoticz.h"
#include "i2c.h"
#include "mqtt.h"
#include "ntp.h"
#include "relay.h"
#include "terminal.h"
#include "thingspeak.h"
#include "rtcmem.h"
#include "ws.h"
#include <cfloat>
#include <cmath>
#include <cstring>
#include <limits>
#include <vector>
//--------------------------------------------------------------------------------
namespace {
#include "filters/LastFilter.h"
#include "filters/MaxFilter.h"
#include "filters/MedianFilter.h"
#include "filters/MovingAverageFilter.h"
#include "filters/SumFilter.h"
} // namespace
#include "sensors/BaseSensor.h"
#include "sensors/BaseEmonSensor.h"
#include "sensors/BaseAnalogEmonSensor.h"
#include "sensors/BaseAnalogSensor.h"
#if AM2320_SUPPORT
#include "sensors/AM2320Sensor.h"
#endif
#if ANALOG_SUPPORT
#include "sensors/AnalogSensor.h"
#endif
#if BH1750_SUPPORT
#include "sensors/BH1750Sensor.h"
#endif
#if BMP180_SUPPORT
#include "sensors/BMP180Sensor.h"
#endif
#if BMX280_SUPPORT
#include "sensors/BMX280Sensor.h"
#endif
#if BME680_SUPPORT
#include "sensors/BME680Sensor.h"
#endif
#if CSE7766_SUPPORT
#include "sensors/CSE7766Sensor.h"
#endif
#if DALLAS_SUPPORT
#include "sensors/DallasSensor.h"
#endif
#if DHT_SUPPORT
#include "sensors/DHTSensor.h"
#endif
#if DIGITAL_SUPPORT
#include "sensors/DigitalSensor.h"
#endif
#if ECH1560_SUPPORT
#include "sensors/ECH1560Sensor.h"
#endif
#if EMON_ADC121_SUPPORT
#include "sensors/EmonADC121Sensor.h"
#endif
#if EMON_ADS1X15_SUPPORT
#include "sensors/EmonADS1X15Sensor.h"
#endif
#if EMON_ANALOG_SUPPORT
#include "sensors/EmonAnalogSensor.h"
#endif
#if EVENTS_SUPPORT
#include "sensors/EventSensor.h"
#endif
#if EZOPH_SUPPORT
#include "sensors/EZOPHSensor.h"
#endif
#if GEIGER_SUPPORT
#include "sensors/GeigerSensor.h"
#endif
#if GUVAS12SD_SUPPORT
#include "sensors/GUVAS12SDSensor.h"
#endif
#if HLW8012_SUPPORT
#include "sensors/HLW8012Sensor.h"
#endif
#if LDR_SUPPORT
#include "sensors/LDRSensor.h"
#endif
#if MAX6675_SUPPORT
#include "sensors/MAX6675Sensor.h"
#endif
#if MICS2710_SUPPORT
#include "sensors/MICS2710Sensor.h"
#endif
#if MICS5525_SUPPORT
#include "sensors/MICS5525Sensor.h"
#endif
#if MHZ19_SUPPORT
#include "sensors/MHZ19Sensor.h"
#endif
#if NTC_SUPPORT
#include "sensors/NTCSensor.h"
#endif
#if SDS011_SUPPORT
#include "sensors/SDS011Sensor.h"
#endif
#if SENSEAIR_SUPPORT
#include "sensors/SenseAirSensor.h"
#endif
#if PMSX003_SUPPORT
#include "sensors/PMSX003Sensor.h"
#endif
#if PULSEMETER_SUPPORT
#include "sensors/PulseMeterSensor.h"
#endif
#if PZEM004T_SUPPORT
#include "sensors/PZEM004TSensor.h"
#endif
#if SHT3X_I2C_SUPPORT
#include "sensors/SHT3XI2CSensor.h"
#endif
#if SI7021_SUPPORT
#include "sensors/SI7021Sensor.h"
#endif
#if SM300D2_SUPPORT
#include "sensors/SM300D2Sensor.h"
#endif
#if SONAR_SUPPORT
#include "sensors/SonarSensor.h"
#endif
#if T6613_SUPPORT
#include "sensors/T6613Sensor.h"
#endif
#if TMP3X_SUPPORT
#include "sensors/TMP3XSensor.h"
#endif
#if V9261F_SUPPORT
#include "sensors/V9261FSensor.h"
#endif
#if VEML6075_SUPPORT
#include "sensors/VEML6075Sensor.h"
#endif
#if VL53L1X_SUPPORT
#include "sensors/VL53L1XSensor.h"
#endif
#if ADE7953_SUPPORT
#include "sensors/ADE7953Sensor.h"
#endif
#if SI1145_SUPPORT
#include "sensors/SI1145Sensor.h"
#endif
#if HDC1080_SUPPORT
#include "sensors/HDC1080Sensor.h"
#endif
#if PZEM004TV30_SUPPORT
// TODO: this is temporary, until we have external API giving us swserial stream objects
#include <SoftwareSerial.h>
#include "sensors/PZEM004TV30Sensor.h"
#endif
//--------------------------------------------------------------------------------
namespace {
class sensor_magnitude_t {
private:
static unsigned char _counts[MAGNITUDE_MAX];
sensor_magnitude_t& operator=(const sensor_magnitude_t&) = default;
void move(sensor_magnitude_t&& other) noexcept {
*this = other;
other.filter = nullptr;
}
public:
static size_t counts(unsigned char type) {
return _counts[type];
}
sensor_magnitude_t() = delete;
sensor_magnitude_t(const sensor_magnitude_t&) = delete;
sensor_magnitude_t(sensor_magnitude_t&& other) noexcept {
*this = other;
other.filter = nullptr;
}
sensor_magnitude_t& operator=(sensor_magnitude_t&& other) noexcept {
move(std::move(other));
return *this;
}
~sensor_magnitude_t() noexcept {
delete filter;
}
sensor_magnitude_t(unsigned char slot, unsigned char type, sensor::Unit units, BaseSensor* sensor);
BaseSensor * sensor { nullptr }; // Sensor object
BaseFilter * filter { nullptr }; // Filter object
unsigned char slot { 0u }; // Sensor slot # taken by the magnitude, used to access the measurement
unsigned char type { MAGNITUDE_NONE }; // Type of measurement, returned by the BaseSensor::type(slot)
unsigned char index_global { 0u }; // N'th magnitude of it's type, across all of the active sensors
sensor::Unit units { sensor::Unit::None }; // Units of measurement
unsigned char decimals { 0u }; // Number of decimals in textual representation
double last { sensor::Value::Unknown }; // Last raw value from sensor (unfiltered)
double reported { sensor::Value::Unknown }; // Last reported value
double min_delta { 0.0 }; // Minimum value change to report
double max_delta { 0.0 }; // Maximum value change to report
double correction { 0.0 }; // Value correction (applied when processing)
double zero_threshold { sensor::Value::Unknown }; // Reset value to zero when below threshold (applied when reading)
};
static_assert(
std::is_nothrow_move_constructible<sensor_magnitude_t>::value,
"std::vector<sensor_magnitude_t> should be able to work with resize()"
);
static_assert(
!std::is_copy_constructible<sensor_magnitude_t>::value,
"std::vector<sensor_magnitude_t> should only use move ctor"
);
unsigned char sensor_magnitude_t::_counts[MAGNITUDE_MAX] = {0};
} // namespace
namespace sensor {
// Base units
// TODO: implement through a single class and allow direct access to the ::value
KWh::KWh() :
value(0)
{}
KWh::KWh(uint32_t value) :
value(value)
{}
Ws::Ws() :
value(0)
{}
Ws::Ws(uint32_t value) :
value(value)
{}
// Generic storage. Most of the time we init this on boot with both members or start at 0 and increment with watt-second
Energy::Energy(KWh kwh, Ws ws) :
kwh(kwh)
{
*this += ws;
}
Energy::Energy(KWh kwh) :
kwh(kwh),
ws()
{}
Energy::Energy(Ws ws) :
kwh()
{
*this += ws;
}
Energy::Energy(double raw) {
*this = raw;
}
Energy& Energy::operator =(double raw) {
double _wh;
kwh = modf(raw, &_wh);
ws = _wh * 3600.0;
return *this;
}
Energy& Energy::operator +=(Ws _ws) {
while (_ws.value >= KwhMultiplier) {
_ws.value -= KwhMultiplier;
++kwh.value;
}
ws.value += _ws.value;
while (ws.value >= KwhMultiplier) {
ws.value -= KwhMultiplier;
++kwh.value;
}
return *this;
}
Energy Energy::operator +(Ws watt_s) {
Energy result(*this);
result += watt_s;
return result;
}
Energy::operator bool() const {
return (kwh.value > 0) && (ws.value > 0);
}
Ws Energy::asWs() const {
auto _kwh = kwh.value;
while (_kwh >= KwhLimit) {
_kwh -= KwhLimit;
}
return (_kwh * KwhMultiplier) + ws.value;
}
double Energy::asDouble() const {
return (double)kwh.value + ((double)ws.value / (double)KwhMultiplier);
}
// Format is `<kwh>+<ws>`
// Value without `+` is treated as `<ws>`
// (internally, we *can* overflow ws that is converted into kwh)
String Energy::asString() const {
String out;
out.reserve(32);
out += kwh.value;
out += '+';
out += ws.value;
return out;
}
void Energy::reset() {
kwh.value = 0;
ws.value = 0;
}
namespace convert {
namespace temperature {
namespace {
struct Base {
constexpr Base() = default;
constexpr explicit Base(double value) :
_value(value)
{}
constexpr double value() const {
return _value;
}
constexpr operator double() const {
return _value;
}
private:
double _value { 0.0 };
};
struct Kelvin : public Base {
using Base::Base;
};
struct Farenheit : public Base {
using Base::Base;
};
struct Celcius : public Base {
using Base::Base;
};
static constexpr Celcius AbsoluteZero { -273.15 };
namespace internal {
template <typename To, typename From>
struct Converter;
static constexpr double celcius_to_kelvin(double celcius) {
return celcius - AbsoluteZero;
}
static constexpr double celcius_to_farenheit(double celcius) {
return (celcius * (9.0 / 5.0)) + 32.0;
}
static constexpr double farenheit_to_celcius(double farenheit) {
return (farenheit - 32.0) * (5.0 / 9.0);
}
static constexpr double farenheit_to_kelvin(double farenheit) {
return celcius_to_kelvin(farenheit_to_celcius(farenheit));
}
static constexpr double kelvin_to_celcius(double kelvin) {
return kelvin + AbsoluteZero;
}
static constexpr double kelvin_to_farenheit(double kelvin) {
return celcius_to_farenheit(kelvin_to_celcius(kelvin));
}
static_assert(celcius_to_kelvin(kelvin_to_celcius(0.0)) == 0.0, "");
static_assert(celcius_to_farenheit(farenheit_to_celcius(0.0)) == 0.0, "");
static_assert(farenheit_to_kelvin(kelvin_to_farenheit(0.0)) == 0.0, "");
static_assert(farenheit_to_celcius(celcius_to_farenheit(0.0)) == 0.0, "");
static_assert(kelvin_to_celcius(celcius_to_kelvin(0.0)) == 0.0, "");
// ref. https://en.cppreference.com/w/cpp/types/numeric_limits/epsilon
static constexpr bool almost_equal(double lhs, double rhs, int ulp) {
// the machine epsilon has to be scaled to the magnitude of the values used
// and multiplied by the desired precision in ULPs (units in the last place)
return __builtin_fabs(lhs - rhs) <= std::numeric_limits<double>::epsilon() * __builtin_fabs(lhs + rhs) * ulp
// unless the result is subnormal
|| __builtin_fabs(lhs - rhs) < std::numeric_limits<double>::min();
}
static_assert(almost_equal(10.0, kelvin_to_farenheit(farenheit_to_kelvin(10.0)), 3), "");
template <>
struct Converter<Kelvin, Kelvin> {
static constexpr Kelvin convert(Kelvin kelvin) {
return kelvin;
}
};
template <>
struct Converter<Celcius, Kelvin> {
static constexpr Celcius convert(Kelvin kelvin) {
return Celcius{ kelvin_to_celcius(kelvin.value()) };
}
};
template <>
struct Converter<Farenheit, Kelvin> {
static constexpr Farenheit convert(Kelvin kelvin) {
return Farenheit{ kelvin_to_farenheit(kelvin.value()) };
}
};
template <>
struct Converter<Celcius, Celcius> {
static constexpr Celcius convert(Celcius celcius) {
return celcius;
}
};
template <>
struct Converter<Kelvin, Celcius> {
static constexpr Kelvin convert(Celcius celcius) {
return Kelvin{ celcius_to_kelvin(celcius.value()) };
}
};
template <>
struct Converter<Farenheit, Celcius> {
static constexpr Farenheit convert(Celcius celcius) {
return Farenheit{ celcius_to_farenheit(celcius.value()) };
}
};
template <>
struct Converter<Farenheit, Farenheit> {
static constexpr Farenheit convert(Farenheit farenheit) {
return farenheit;
}
};
template <>
struct Converter<Kelvin, Farenheit> {
static constexpr Kelvin convert(Farenheit farenheit) {
return Kelvin{ farenheit_to_kelvin(farenheit.value()) };
}
};
template <>
struct Converter<Celcius, Farenheit> {
static constexpr Celcius convert(Farenheit farenheit) {
return Celcius{ farenheit_to_celcius(farenheit.value()) };
}
};
// just some sanity checks. note that floating point will not always produce exact results
// (and it might not be a good idea to actually have anything compare with the Farenheit one)
static_assert(Converter<Kelvin, Kelvin>::convert(Kelvin{0.0}) == Kelvin{0.0}, "");
static_assert(Converter<Celcius, Celcius>::convert(AbsoluteZero) == AbsoluteZero, "");
} // namespace internal
template <typename To, typename From>
constexpr To unit_cast(From value) {
return internal::Converter<To, From>::convert(value);
}
static_assert(unit_cast<Kelvin>(AbsoluteZero).value() == 0.0, "");
static_assert(unit_cast<Celcius>(AbsoluteZero).value() == AbsoluteZero.value(), "");
// since the outside api only works with the enumeration, make sure to cast it to our types for conversion
// a table like this could've also worked
// > {sensor::Unit(from), sensor::Unit(to), Converter(double(*)(double))}
// but, it is ~0.6KiB vs. ~0.1KiB for this one. plus, some obstacles with c++11 implementation
// although, there may be a way to make this cheaper in both compile-time and runtime
// attempt to convert the input value from one unit to the other
// will return the input value when units match or there's no known conversion
static constexpr double convert(double value, sensor::Unit from, sensor::Unit to) {
#define UNIT_CAST(FROM, TO) \
((from == sensor::Unit::FROM) && (to == sensor::Unit::TO)) \
? (unit_cast<TO>(FROM{value}))
return UNIT_CAST(Kelvin, Kelvin) :
UNIT_CAST(Kelvin, Celcius) :
UNIT_CAST(Kelvin, Farenheit) :
UNIT_CAST(Celcius, Celcius) :
UNIT_CAST(Celcius, Kelvin) :
UNIT_CAST(Celcius, Farenheit) :
UNIT_CAST(Farenheit, Farenheit) :
UNIT_CAST(Farenheit, Kelvin) :
UNIT_CAST(Farenheit, Celcius) : value;
#undef UNIT_CAST
}
} // namespace
} // namespace temperature
} // namespace convert
namespace build {
namespace {
constexpr double DefaultMinDelta { 0.0 };
constexpr double DefaultMaxDelta { 0.0 };
constexpr espurna::duration::Seconds initInterval() {
return espurna::duration::Seconds(SENSOR_INIT_INTERVAL);
}
constexpr espurna::duration::Seconds ReadIntervalMin { SENSOR_READ_MIN_INTERVAL };
constexpr espurna::duration::Seconds ReadIntervalMax { SENSOR_READ_MAX_INTERVAL };
constexpr espurna::duration::Seconds readInterval() {
return espurna::duration::Seconds(SENSOR_READ_INTERVAL);
}
constexpr int ReportEveryMin { SENSOR_REPORT_MIN_EVERY };
constexpr int ReportEveryMax { SENSOR_REPORT_MAX_EVERY };
constexpr int reportEvery() {
return SENSOR_REPORT_EVERY;
}
constexpr int saveEvery() {
return SENSOR_SAVE_EVERY;
}
constexpr bool realTimeValues() {
return SENSOR_REAL_TIME_VALUES;
}
} // namespace
} // namespace build
namespace settings {
namespace prefix {
namespace {
alignas(4) static constexpr char Sensor[] PROGMEM = "sns";
alignas(4) static constexpr char Power[] PROGMEM = "pwr";
alignas(4) static constexpr char Temperature[] = "tmp";
alignas(4) static constexpr char Humidity[] = "hum";
alignas(4) static constexpr char Pressure[] = "press";
alignas(4) static constexpr char Current[] = "curr";
alignas(4) static constexpr char Voltage[] = "volt";
alignas(4) static constexpr char PowerActive[] = "pwrP";
alignas(4) static constexpr char PowerApparent[] = "pwrQ";
alignas(4) static constexpr char PowerReactive[] = "pwrModS";
alignas(4) static constexpr char PowerFactor[] = "pwrPF";
alignas(4) static constexpr char Energy[] = "ene";
alignas(4) static constexpr char EnergyDelta[] = "eneDelta";
alignas(4) static constexpr char Analog[] = "analog";
alignas(4) static constexpr char Digital[] = "digital";
alignas(4) static constexpr char Event[] = "event";
alignas(4) static constexpr char Pm1Dot0[] = "pm1dot0";
alignas(4) static constexpr char Pm2Dot5[] = "pm2dot5";
alignas(4) static constexpr char Pm10[] = "pm10";
alignas(4) static constexpr char Co2[] = "co2";
alignas(4) static constexpr char Voc[] = "voc";
alignas(4) static constexpr char Iaq[] = "iaq";
alignas(4) static constexpr char IaqAccuracy[] = "iaqAccuracy";
alignas(4) static constexpr char IaqStatic[] = "iaqStatic";
alignas(4) static constexpr char Lux[] = "lux";
alignas(4) static constexpr char Uva[] = "uva";
alignas(4) static constexpr char Uvb[] = "uvb";
alignas(4) static constexpr char Uvi[] = "uvi";
alignas(4) static constexpr char Distance[] = "distance";
alignas(4) static constexpr char Hcho[] = "hcho";
alignas(4) static constexpr char GeigerCpm[] = "gcpm";
alignas(4) static constexpr char GeigerSievert[] = "gsiev";
alignas(4) static constexpr char Count[] = "count";
alignas(4) static constexpr char No2[] = "no2";
alignas(4) static constexpr char Co[] = "co";
alignas(4) static constexpr char Resistance[] = "res";
alignas(4) static constexpr char Ph[] = "ph";
alignas(4) static constexpr char Frequency[] = "freq";
alignas(4) static constexpr char Tvoc[] = "tvoc";
alignas(4) static constexpr char Ch2o[] = "ch2o";
alignas(4) static constexpr char Unknown[] = "unknown";
constexpr ::settings::StringView get(unsigned char type) {
return (type == MAGNITUDE_TEMPERATURE) ? Temperature :
(type == MAGNITUDE_HUMIDITY) ? Humidity :
(type == MAGNITUDE_PRESSURE) ? Pressure :
(type == MAGNITUDE_CURRENT) ? Current :
(type == MAGNITUDE_VOLTAGE) ? Voltage :
(type == MAGNITUDE_POWER_ACTIVE) ? PowerActive :
(type == MAGNITUDE_POWER_APPARENT) ? PowerApparent :
(type == MAGNITUDE_POWER_REACTIVE) ? PowerReactive :
(type == MAGNITUDE_POWER_FACTOR) ? PowerFactor :
(type == MAGNITUDE_ENERGY) ? Energy :
(type == MAGNITUDE_ENERGY_DELTA) ? EnergyDelta :
(type == MAGNITUDE_ANALOG) ? Analog :
(type == MAGNITUDE_DIGITAL) ? Digital :
(type == MAGNITUDE_EVENT) ? Event :
(type == MAGNITUDE_PM1DOT0) ? Pm1Dot0 :
(type == MAGNITUDE_PM2DOT5) ? Pm2Dot5 :
(type == MAGNITUDE_PM10) ? Pm10 :
(type == MAGNITUDE_CO2) ? Co2 :
(type == MAGNITUDE_VOC) ? Voc :
(type == MAGNITUDE_IAQ) ? Iaq :
(type == MAGNITUDE_IAQ_ACCURACY) ? IaqAccuracy :
(type == MAGNITUDE_IAQ_STATIC) ? IaqStatic :
(type == MAGNITUDE_LUX) ? Lux :
(type == MAGNITUDE_UVA) ? Uva :
(type == MAGNITUDE_UVB) ? Uvb :
(type == MAGNITUDE_UVI) ? Uvi :
(type == MAGNITUDE_DISTANCE) ? Distance :
(type == MAGNITUDE_HCHO) ? Hcho :
(type == MAGNITUDE_GEIGER_CPM) ? GeigerCpm :
(type == MAGNITUDE_GEIGER_SIEVERT) ? GeigerSievert :
(type == MAGNITUDE_COUNT) ? Count :
(type == MAGNITUDE_NO2) ? No2 :
(type == MAGNITUDE_CO) ? Co :
(type == MAGNITUDE_RESISTANCE) ? Resistance :
(type == MAGNITUDE_PH) ? Ph :
(type == MAGNITUDE_FREQUENCY) ? Frequency :
(type == MAGNITUDE_TVOC) ? Tvoc :
(type == MAGNITUDE_CH2O) ? Ch2o :
Unknown;
}
} // namespace
} // namespace prefix
namespace suffix {
namespace {
alignas(4) static constexpr char Units[] PROGMEM = "Units";
alignas(4) static constexpr char Ratio[] PROGMEM = "Ratio";
alignas(4) static constexpr char Correction[] PROGMEM = "Correction";
} // namespace
} // namespace suffix
namespace keys {
namespace {
alignas(4) static constexpr char ReadInterval[] PROGMEM = "snsRead";
alignas(4) static constexpr char InitInterval[] PROGMEM = "snsInit";
alignas(4) static constexpr char ReportEvery[] PROGMEM = "snsReport";
alignas(4) static constexpr char SaveEvery[] PROGMEM = "snsSave";
alignas(4) static constexpr char RealTimeValues[] PROGMEM = "snsRealTime";
} // namespace
} // namespace keys
namespace {
espurna::duration::Seconds readInterval() {
return std::clamp(getSetting(FPSTR(keys::ReadInterval), build::readInterval()),
build::ReadIntervalMin, build::ReadIntervalMax);
}
espurna::duration::Seconds initInterval() {
return std::clamp(getSetting(FPSTR(keys::InitInterval), build::initInterval()),
build::ReadIntervalMin, build::ReadIntervalMax);
}
int reportEvery() {
return std::clamp(getSetting(FPSTR(keys::ReportEvery), build::reportEvery()),
build::ReportEveryMin, build::ReportEveryMax);
}
int saveEvery() {
return getSetting(FPSTR(keys::SaveEvery), build::saveEvery());
}
bool realTimeValues() {
return getSetting(FPSTR(keys::RealTimeValues), build::realTimeValues());
}
} // namespace
} // namespace settings
} // namespace sensor
namespace settings {
namespace internal {
namespace {
alignas(4) static constexpr char Farenheit[] PROGMEM = "°F";
alignas(4) static constexpr char Celcius[] PROGMEM = "°C";
alignas(4) static constexpr char Kelvin[] PROGMEM = "K";
alignas(4) static constexpr char Percentage[] PROGMEM = "%";
alignas(4) static constexpr char Hectopascal[] PROGMEM = "hPa";
alignas(4) static constexpr char Ampere[] PROGMEM = "A";
alignas(4) static constexpr char Volt[] PROGMEM = "V";
alignas(4) static constexpr char Watt[] PROGMEM = "W";
alignas(4) static constexpr char Kilowatt[] PROGMEM = "kW";
alignas(4) static constexpr char Voltampere[] PROGMEM = "VA";
alignas(4) static constexpr char Kilovoltampere[] PROGMEM = "kVA";
alignas(4) static constexpr char VoltampereReactive[] PROGMEM = "VAR";
alignas(4) static constexpr char KilovoltampereReactive[] PROGMEM = "kVAR";
alignas(4) static constexpr char Joule[] PROGMEM = "J";
alignas(4) static constexpr char KilowattHour[] PROGMEM = "kWh";
alignas(4) static constexpr char MicrogrammPerCubicMeter[] PROGMEM = "µg/m³";
alignas(4) static constexpr char PartsPerMillion[] PROGMEM = "ppm";
alignas(4) static constexpr char Lux[] PROGMEM = "lux";
alignas(4) static constexpr char UltravioletIndex[] PROGMEM = "UVindex";
alignas(4) static constexpr char Ohm[] PROGMEM = "ohm";
alignas(4) static constexpr char MilligrammPerCubicMeter[] PROGMEM = "mg/m³";
alignas(4) static constexpr char CountsPerMinute[] PROGMEM = "cpm";
alignas(4) static constexpr char MicrosievertPerHour[] PROGMEM = "µSv/h";
alignas(4) static constexpr char Meter[] PROGMEM = "m";
alignas(4) static constexpr char Hertz[] PROGMEM = "Hz";
alignas(4) static constexpr char Ph[] PROGMEM = "pH";
alignas(4) static constexpr char None[] PROGMEM = "none";
static constexpr ::settings::options::Enumeration<sensor::Unit> SensorUnitOptions[] PROGMEM {
{sensor::Unit::Farenheit, Farenheit},
{sensor::Unit::Celcius, Celcius},
{sensor::Unit::Kelvin, Kelvin},
{sensor::Unit::Percentage, Percentage},
{sensor::Unit::Hectopascal, Hectopascal},
{sensor::Unit::Ampere, Ampere},
{sensor::Unit::Volt, Volt},
{sensor::Unit::Watt, Watt},
{sensor::Unit::Kilowatt, Kilowatt},
{sensor::Unit::Voltampere, Voltampere},
{sensor::Unit::Kilovoltampere, Kilovoltampere},
{sensor::Unit::VoltampereReactive, VoltampereReactive},
{sensor::Unit::KilovoltampereReactive, KilovoltampereReactive},
{sensor::Unit::Joule, Joule},
{sensor::Unit::WattSecond, Joule},
{sensor::Unit::KilowattHour, KilowattHour},
{sensor::Unit::MicrogrammPerCubicMeter, MicrogrammPerCubicMeter},
{sensor::Unit::PartsPerMillion, PartsPerMillion},
{sensor::Unit::Lux, Lux},
{sensor::Unit::UltravioletIndex, UltravioletIndex},
{sensor::Unit::Ohm, Ohm},
{sensor::Unit::MilligrammPerCubicMeter, MilligrammPerCubicMeter},
{sensor::Unit::CountsPerMinute, CountsPerMinute},
{sensor::Unit::MicrosievertPerHour, MicrosievertPerHour},
{sensor::Unit::Meter, Meter},
{sensor::Unit::Hertz, Hertz},
{sensor::Unit::Ph, Ph},
{sensor::Unit::None, None},
};
} // namespace
template <>
sensor::Unit convert(const String& value) {
return convert(SensorUnitOptions, value, sensor::Unit::None);
}
String serialize(sensor::Unit unit) {
return serialize(SensorUnitOptions, unit);
}
} // namespace internal
} // namespace settings
// -----------------------------------------------------------------------------
// Energy persistence
// -----------------------------------------------------------------------------
namespace {
struct SensorEnergyTracker {
using Magnitude = std::reference_wrapper<sensor_magnitude_t>;
struct Counter {
Magnitude magnitude;
int value;
};
using Counters = std::vector<Counter>;
explicit operator bool() const {
return _every > 0;
}
int every() const {
return _every;
}
void add(sensor_magnitude_t& magnitude) {
_count.push_back({magnitude, 0});
}
size_t size() const {
return _count.size();
}
int count(size_t index) const {
return _count[index].value;
}
template <typename Callback>
void tick(unsigned char index, Callback&& callback) {
_count[index].value = (_count[index].value + 1) % _every;
if (_count[index].value == 0) {
callback();
}
}
void every(int every) {
_every = every;
for (auto& count : _count) {
count.value = 0;
}
}
private:
Counters _count;
int _every;
};
SensorEnergyTracker _sensor_energy_tracker;
bool _sensorIsEmon(BaseSensor* sensor) {
return sensor->type() & (sensor::type::Emon | sensor::type::AnalogEmon);
}
bool _sensorIsAnalogEmon(BaseSensor* sensor) {
return sensor->type() & sensor::type::AnalogEmon;
}
bool _sensorIsAnalog(BaseSensor* sensor) {
return sensor->type() & sensor::type::Analog;
}
sensor::Energy _sensorRtcmemLoadEnergy(unsigned char index) {
return sensor::Energy {
sensor::KWh { Rtcmem->energy[index].kwh },
sensor::Ws { Rtcmem->energy[index].ws }
};
}
void _sensorRtcmemSaveEnergy(unsigned char index, const sensor::Energy& source) {
Rtcmem->energy[index].kwh = source.kwh.value;
Rtcmem->energy[index].ws = source.ws.value;
}
struct EnergyParseResult {
EnergyParseResult() = default;
EnergyParseResult& operator=(sensor::Energy value) {
_value = value;
_result = true;
return *this;
}
explicit operator bool() const {
return _result;
}
sensor::Energy value() const {
return _value;
}
private:
bool _result { false };
sensor::Energy _value;
};
EnergyParseResult _sensorParseEnergy(const String& value) {
EnergyParseResult out;
if (!value.length()) {
return out;
}
const char* p { value.c_str() };
char* endp { nullptr };
auto kwh = strtoul(p, &endp, 10);
if (!endp || (endp == p)) {
return out;
}
sensor::Energy energy{};
energy.kwh = kwh;
const char* plus { strchr(p, '+') };
if (plus) {
p = plus + 1;
if (*p == '\0') {
return out;
}
auto ws = strtoul(p, &endp, 10);
if (!endp || (endp == p)) {
return out;
}
energy.ws = ws;
}
out = energy;
return out;
}
void _sensorApiResetEnergy(const sensor_magnitude_t& magnitude, const String& payload) {
if (!payload.length()) {
return;
}
auto energy = _sensorParseEnergy(payload);
if (!energy) {
return;
}
auto* sensor = static_cast<BaseEmonSensor*>(magnitude.sensor);
sensor->resetEnergy(magnitude.slot, energy.value());
}
void _sensorApiResetEnergy(const sensor_magnitude_t& magnitude, const char* payload) {
if (!payload) {
return;
}
_sensorApiResetEnergy(magnitude, payload);
}
sensor::Energy _sensorEnergyTotal(unsigned char index) {
sensor::Energy result;
if (rtcmemStatus() && (index < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy)))) {
result = _sensorRtcmemLoadEnergy(index);
} else {
result = _sensorParseEnergy(getSetting({"eneTotal", index})).value();
}
return result;
}
void _sensorResetEnergyTotal(unsigned char index) {
delSetting({"eneTotal", index});
delSetting({"eneTime", index});
if (index < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy))) {
Rtcmem->energy[index].kwh = 0;
Rtcmem->energy[index].ws = 0;
}
}
struct SensorPersistEnergyTotal {
SensorPersistEnergyTotal(size_t index, sensor::Energy energy) :
_index(index),
_energy(energy)
{}
void operator()() const {
setSetting({"eneTotal", _index}, _energy.asString());
#if NTP_SUPPORT
if (ntpSynced()) {
setSetting({"eneTime", _index}, ntpDateTime());
}
#endif
}
private:
size_t _index;
sensor::Energy _energy;
};
void _magnitudeSaveEnergyTotal(sensor_magnitude_t& magnitude, bool persistent) {
if (magnitude.type != MAGNITUDE_ENERGY) return;
auto* sensor = static_cast<BaseEmonSensor*>(magnitude.sensor);
const auto energy = sensor->totalEnergy(magnitude.slot);
// Always save to RTCMEM
if (magnitude.index_global < (sizeof(Rtcmem->energy) / sizeof(*Rtcmem->energy))) {
_sensorRtcmemSaveEnergy(magnitude.index_global, energy);
}
// Save to EEPROM every '_sensor_save_every' readings
if (persistent && _sensor_energy_tracker) {
_sensor_energy_tracker.tick(magnitude.index_global,
SensorPersistEnergyTotal{magnitude.index_global, energy});
}
}
void _sensorTrackEnergyTotal(sensor_magnitude_t& magnitude) {
const auto index_global = magnitude.index_global;
auto* ptr = static_cast<BaseEmonSensor*>(magnitude.sensor);
ptr->resetEnergy(magnitude.slot, _sensorEnergyTotal(index_global));
_sensor_energy_tracker.add(magnitude);
}
} // namespace
sensor::Energy sensorEnergyTotal() {
return _sensorEnergyTotal(0);
}
// -----------------------------------------------------------------------------
// Data processing
// -----------------------------------------------------------------------------
namespace {
bool _sensors_ready { false };
std::vector<BaseSensor*> _sensors;
bool _sensor_real_time { sensor::build::realTimeValues() };
int _sensor_report_every { sensor::build::reportEvery() };
espurna::duration::Seconds _sensor_read_interval { sensor::build::readInterval() };
espurna::duration::Seconds _sensor_init_interval { sensor::build::initInterval() };
std::vector<sensor_magnitude_t> _magnitudes;
using MagnitudeReadHandlers = std::forward_list<MagnitudeReadHandler>;
MagnitudeReadHandlers _magnitude_read_handlers;
MagnitudeReadHandlers _magnitude_report_handlers;
BaseFilter* _magnitudeCreateFilter(unsigned char type, size_t size) {
BaseFilter* filter { nullptr };
switch (type) {
case MAGNITUDE_IAQ:
case MAGNITUDE_IAQ_STATIC:
case MAGNITUDE_ENERGY:
filter = new LastFilter();
break;
case MAGNITUDE_COUNT:
case MAGNITUDE_GEIGER_CPM:
case MAGNITUDE_GEIGER_SIEVERT:
case MAGNITUDE_ENERGY_DELTA:
filter = new SumFilter();
break;
case MAGNITUDE_EVENT:
case MAGNITUDE_DIGITAL:
filter = new MaxFilter();
break;
default:
filter = new MedianFilter();
break;
}
filter->resize(size);
return filter;
}
sensor_magnitude_t::sensor_magnitude_t(unsigned char slot_, unsigned char type_, sensor::Unit units_, BaseSensor* sensor_) :
sensor(sensor_),
filter(_magnitudeCreateFilter(type_, _sensor_report_every)),
slot(slot_),
type(type_),
index_global(_counts[type]),
units(units_)
{
++_counts[type];
}
// Hardcoded decimals for each magnitude
unsigned char _sensorUnitDecimals(sensor::Unit unit) {
switch (unit) {
case sensor::Unit::Celcius:
case sensor::Unit::Farenheit:
return 1;
case sensor::Unit::Percentage:
return 0;
case sensor::Unit::Hectopascal:
return 2;
case sensor::Unit::Ampere:
return 3;
case sensor::Unit::Volt:
return 0;
case sensor::Unit::Watt:
case sensor::Unit::Voltampere:
case sensor::Unit::VoltampereReactive:
return 0;
case sensor::Unit::Kilowatt:
case sensor::Unit::Kilovoltampere:
case sensor::Unit::KilovoltampereReactive:
return 3;
case sensor::Unit::KilowattHour:
return 3;
case sensor::Unit::WattSecond:
return 0;
case sensor::Unit::CountsPerMinute:
case sensor::Unit::MicrosievertPerHour:
return 4;
case sensor::Unit::Meter:
return 3;
case sensor::Unit::Hertz:
return 1;
case sensor::Unit::UltravioletIndex:
return 3;
case sensor::Unit::Ph:
return 3;
case sensor::Unit::None:
default:
return 0;
}
}
String _magnitudeTopic(unsigned char type) {
const __FlashStringHelper* result = nullptr;
switch (type) {
case MAGNITUDE_TEMPERATURE:
result = F("temperature");
break;
case MAGNITUDE_HUMIDITY:
result = F("humidity");
break;
case MAGNITUDE_PRESSURE:
result = F("pressure");
break;
case MAGNITUDE_CURRENT:
result = F("current");
break;
case MAGNITUDE_VOLTAGE:
result = F("voltage");
break;
case MAGNITUDE_POWER_ACTIVE:
result = F("power");
break;
case MAGNITUDE_POWER_APPARENT:
result = F("apparent");
break;
case MAGNITUDE_POWER_REACTIVE:
result = F("reactive");
break;
case MAGNITUDE_POWER_FACTOR:
result = F("factor");
break;
case MAGNITUDE_ENERGY:
result = F("energy");
break;
case MAGNITUDE_ENERGY_DELTA:
result = F("energy_delta");
break;
case MAGNITUDE_ANALOG:
result = F("analog");
break;
case MAGNITUDE_DIGITAL:
result = F("digital");
break;
case MAGNITUDE_EVENT:
result = F("event");
break;
case MAGNITUDE_PM1DOT0:
result = F("pm1dot0");
break;
case MAGNITUDE_PM2DOT5:
result = F("pm2dot5");
break;
case MAGNITUDE_PM10:
result = F("pm10");
break;
case MAGNITUDE_CO2:
result = F("co2");
break;
case MAGNITUDE_VOC:
result = F("voc");
break;
case MAGNITUDE_IAQ:
result = F("iaq");
break;
case MAGNITUDE_IAQ_ACCURACY:
result = F("iaq_accuracy");
break;
case MAGNITUDE_IAQ_STATIC:
result = F("iaq_static");
break;
case MAGNITUDE_LUX:
result = F("lux");
break;
case MAGNITUDE_UVA:
result = F("uva");
break;
case MAGNITUDE_UVB:
result = F("uvb");
break;
case MAGNITUDE_UVI:
result = F("uvi");
break;
case MAGNITUDE_DISTANCE:
result = F("distance");
break;
case MAGNITUDE_HCHO:
result = F("hcho");
break;
case MAGNITUDE_GEIGER_CPM:
result = F("ldr_cpm"); // local dose rate [Counts per minute]
break;
case MAGNITUDE_GEIGER_SIEVERT:
result = F("ldr_uSvh"); // local dose rate [µSievert per hour]
break;
case MAGNITUDE_COUNT:
result = F("count");
break;
case MAGNITUDE_NO2:
result = F("no2");
break;
case MAGNITUDE_CO:
result = F("co");
break;
case MAGNITUDE_RESISTANCE:
result = F("resistance");
break;
case MAGNITUDE_PH:
result = F("ph");
break;
case MAGNITUDE_FREQUENCY:
result = F("frequency");
break;
case MAGNITUDE_TVOC:
result = F("tvoc");
break;
case MAGNITUDE_CH2O:
result = F("ch2o");
break;
case MAGNITUDE_NONE:
default:
result = F("unknown");
break;
}
return String(result);
}
String _magnitudeUnits(sensor::Unit unit) {
return ::settings::internal::serialize(unit);
}
String _magnitudeUnits(const sensor_magnitude_t& magnitude) {
return _magnitudeUnits(magnitude.units);
}
} // namespace
String magnitudeUnits(unsigned char index) {
if (index < _magnitudes.size()) {
return _magnitudeUnits(_magnitudes[index]);
}
return String();
}
namespace {
// Choose unit based on type of magnitude we use
struct MagnitudeUnitsRange {
MagnitudeUnitsRange() = default;
template <size_t Size>
explicit MagnitudeUnitsRange(const sensor::Unit (&units)[Size]) :
_begin(std::begin(units)),
_end(std::end(units))
{}
template <size_t Size>
MagnitudeUnitsRange& operator=(const sensor::Unit (&units)[Size]) {
_begin = std::begin(units);
_end = std::end(units);
return *this;
}
const sensor::Unit* begin() const {
return _begin;
}
const sensor::Unit* end() const {
return _end;
}
private:
const sensor::Unit* _begin { nullptr };
const sensor::Unit* _end { nullptr };
};
#define MAGNITUDE_UNITS_RANGE(...)\
static const sensor::Unit units[] PROGMEM {\
__VA_ARGS__\
};\
\
out = units
MagnitudeUnitsRange _magnitudeUnitsRange(unsigned char type) {
MagnitudeUnitsRange out;
switch (type) {
case MAGNITUDE_TEMPERATURE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Celcius,
sensor::Unit::Farenheit,
sensor::Unit::Kelvin
);
break;
}
case MAGNITUDE_HUMIDITY:
case MAGNITUDE_POWER_FACTOR: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Percentage
);
break;
}
case MAGNITUDE_PRESSURE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Hectopascal
);
break;
}
case MAGNITUDE_CURRENT: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Ampere
);
break;
}
case MAGNITUDE_VOLTAGE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Volt
);
break;
}
case MAGNITUDE_POWER_ACTIVE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Watt,
sensor::Unit::Kilowatt
);
break;
}
case MAGNITUDE_POWER_APPARENT: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Voltampere,
sensor::Unit::Kilovoltampere
);
break;
}
case MAGNITUDE_POWER_REACTIVE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::VoltampereReactive,
sensor::Unit::KilovoltampereReactive
);
break;
}
case MAGNITUDE_ENERGY_DELTA: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Joule
);
break;
}
case MAGNITUDE_ENERGY: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Joule,
sensor::Unit::KilowattHour
);
break;
}
case MAGNITUDE_PM1DOT0:
case MAGNITUDE_PM2DOT5:
case MAGNITUDE_PM10:
case MAGNITUDE_TVOC:
case MAGNITUDE_CH2O: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::MicrogrammPerCubicMeter,
sensor::Unit::MilligrammPerCubicMeter
);
break;
}
case MAGNITUDE_CO:
case MAGNITUDE_CO2:
case MAGNITUDE_NO2:
case MAGNITUDE_VOC: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::PartsPerMillion
);
break;
}
case MAGNITUDE_LUX: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Lux
);
break;
}
case MAGNITUDE_RESISTANCE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Ohm
);
break;
}
case MAGNITUDE_HCHO: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::MilligrammPerCubicMeter
);
break;
}
case MAGNITUDE_GEIGER_CPM: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::CountsPerMinute
);
break;
}
case MAGNITUDE_GEIGER_SIEVERT: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::MicrosievertPerHour
);
break;
}
case MAGNITUDE_DISTANCE: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Meter
);
break;
}
case MAGNITUDE_FREQUENCY: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Hertz
);
break;
}
case MAGNITUDE_PH: {
MAGNITUDE_UNITS_RANGE(
sensor::Unit::Ph
);
break;
}
}
return out;
}
bool _magnitudeUnitSupported(const sensor_magnitude_t& magnitude, sensor::Unit unit) {
const auto range = _magnitudeUnitsRange(magnitude.type);
return std::any_of(range.begin(), range.end(), [&](sensor::Unit supported) {
return (unit == supported);
});
}
sensor::Unit _magnitudeUnitFilter(const sensor_magnitude_t& magnitude, sensor::Unit unit) {
return _magnitudeUnitSupported(magnitude, unit) ? unit : magnitude.units;
}
double _magnitudeProcess(const sensor_magnitude_t& magnitude, double value) {
// Process input (sensor) units and convert to the ones that magnitude specifies as output
const auto source = magnitude.sensor->units(magnitude.slot);
switch (source) {
case sensor::Unit::Farenheit:
case sensor::Unit::Kelvin:
case sensor::Unit::Celcius:
value = sensor::convert::temperature::convert(value, source, magnitude.units);
break;
case sensor::Unit::Percentage:
value = std::clamp(value, 0.0, 100.0);
break;
case sensor::Unit::Watt:
case sensor::Unit::Voltampere:
case sensor::Unit::VoltampereReactive:
if ((magnitude.units == sensor::Unit::Kilowatt)
|| (magnitude.units == sensor::Unit::Kilovoltampere)
|| (magnitude.units == sensor::Unit::KilovoltampereReactive)) {
value = value / 1.0e+3;
}
break;
case sensor::Unit::KilowattHour:
// TODO: we may end up with inf at some point?
if (magnitude.units == sensor::Unit::Joule) {
value = value * 3.6e+6;
}
break;
default:
break;
}
value = value + magnitude.correction;
return roundTo(value, magnitude.decimals);
}
String _magnitudeDescription(const sensor_magnitude_t& magnitude) {
return magnitude.sensor->description(magnitude.slot);
}
// -----------------------------------------------------------------------------
// do `callback(type)` for each present magnitude
template <typename T>
void _magnitudeForEachCounted(T&& callback) {
for (unsigned char type = MAGNITUDE_NONE + 1; type < MAGNITUDE_MAX; ++type) {
if (sensor_magnitude_t::counts(type)) {
callback(type);
}
}
}
// check if `callback(type)` returns `true` at least once
template <typename T>
bool _magnitudeForEachCountedCheck(T&& callback) {
for (unsigned char type = MAGNITUDE_NONE + 1; type < MAGNITUDE_MAX; ++type) {
if (sensor_magnitude_t::counts(type) && callback(type)) {
return true;
}
}
return false;
}
// do `callback(type)` for each error type
template <typename T>
void _sensorForEachError(T&& callback) {
for (unsigned char error = SENSOR_ERROR_OK; error < SENSOR_ERROR_MAX; ++error) {
callback(error);
}
}
const __FlashStringHelper* _magnitudeSettingsPrefix(unsigned char type) {
return FPSTR(sensor::settings::prefix::get(type).c_str());
}
template <typename T>
String _magnitudeSettingsKey(unsigned char type, T&& suffix) {
return String(_magnitudeSettingsPrefix(type)) + std::forward<T>(suffix);
}
template <typename T>
String _magnitudeSettingsKey(sensor_magnitude_t& magnitude, T&& suffix) {
return _magnitudeSettingsKey(magnitude.type, std::forward<T>(suffix));
}
constexpr double _magnitudeCorrection(unsigned char type) {
return (
(MAGNITUDE_TEMPERATURE == type) ? (SENSOR_TEMPERATURE_CORRECTION) :
(MAGNITUDE_HUMIDITY == type) ? (SENSOR_HUMIDITY_CORRECTION) :
(MAGNITUDE_LUX == type) ? (SENSOR_LUX_CORRECTION) :
(MAGNITUDE_PRESSURE == type) ? (SENSOR_PRESSURE_CORRECTION) :
0.0
);
}
constexpr bool _magnitudeCorrectionSupported(unsigned char type) {
return (MAGNITUDE_TEMPERATURE == type)
|| (MAGNITUDE_HUMIDITY == type)
|| (MAGNITUDE_LUX == type)
|| (MAGNITUDE_PRESSURE == type);
}
SettingsKey _magnitudeSettingsCorrectionKey(unsigned char type, size_t index) {
return {_magnitudeSettingsKey(type, FPSTR(sensor::settings::suffix::Correction)), index};
}
SettingsKey _magnitudeSettingsCorrectionKey(const sensor_magnitude_t& magnitude) {
return _magnitudeSettingsCorrectionKey(magnitude.type, magnitude.index_global);
}
bool _sensorCheckKeyPrefix(::settings::StringView key) {
if (key.length() < 3) {
return false;
}
using settings::query::samePrefix;
using settings::StringView;
if (samePrefix(key, sensor::settings::prefix::Sensor)) {
return true;
}
if (samePrefix(key, sensor::settings::prefix::Power)) {
return true;
}
return _magnitudeForEachCountedCheck([&](unsigned char type) {
return samePrefix(key, StringView{_magnitudeSettingsPrefix(type)});
});
}
SettingsKey _magnitudeSettingsRatioKey(unsigned char type, size_t index) {
return {_magnitudeSettingsKey(type, FPSTR(sensor::settings::suffix::Ratio)), index};
}
SettingsKey _magnitudeSettingsRatioKey(const sensor_magnitude_t& magnitude) {
return _magnitudeSettingsRatioKey(magnitude.type, magnitude.index_global);
}
double _magnitudeSettingsRatio(const sensor_magnitude_t& magnitude, double defaultValue) {
return getSetting(_magnitudeSettingsRatioKey(magnitude), defaultValue);
};
constexpr bool _magnitudeRatioSupported(unsigned char type) {
return (type == MAGNITUDE_CURRENT)
|| (type == MAGNITUDE_VOLTAGE)
|| (type == MAGNITUDE_POWER_ACTIVE)
|| (type == MAGNITUDE_ENERGY);
}
SettingsKey _magnitudeSettingsUnitsKey(unsigned char type, size_t index) {
return {_magnitudeSettingsKey(type, FPSTR(sensor::settings::suffix::Units)), index};
}
SettingsKey _magnitudeSettingsUnitsKey(const sensor_magnitude_t& magnitude) {
return _magnitudeSettingsUnitsKey(magnitude.type, magnitude.index_global);
}
String _sensorQueryHandler(::settings::StringView key) {
String out;
for (auto& magnitude : _magnitudes) {
if (_magnitudeRatioSupported(magnitude.type)) {
auto expected = _magnitudeSettingsRatioKey(magnitude);
if (key == expected) {
out = String(reinterpret_cast<BaseEmonSensor*>(magnitude.sensor)->defaultRatio(magnitude.slot));
break;
}
}
if (_magnitudeCorrectionSupported(magnitude.type)) {
auto expected = _magnitudeSettingsCorrectionKey(magnitude);
if (key == expected) {
out = String(magnitude.correction);
break;
}
}
auto expected = _magnitudeSettingsUnitsKey(magnitude);
if (key == expected) {
out = ::settings::internal::serialize(magnitude.units);
break;
}
}
return out;
}
} // namespace
// -----------------------------------------------------------------------------
// Sensor calibration & emon ratios
// -----------------------------------------------------------------------------
namespace {
void _sensorAnalogInit(BaseAnalogSensor* sensor) {
sensor->setR0(getSetting("snsR0", sensor->getR0()));
sensor->setRS(getSetting("snsRS", sensor->getRS()));
sensor->setRL(getSetting("snsRL", sensor->getRL()));
}
void _sensorApiAnalogCalibrate() {
for (auto& ptr : _sensors) {
if (_sensorIsAnalog(ptr)) {
DEBUG_MSG_P(PSTR("[ANALOG] Calibrating %s\n"), ptr->description().c_str());
auto* sensor = static_cast<BaseAnalogSensor*>(ptr);
sensor->calibrate();
setSetting("snsR0", sensor->getR0());
break;
}
}
}
void _sensorApiEmonResetRatios() {
static constexpr unsigned char types[] {
MAGNITUDE_CURRENT,
MAGNITUDE_VOLTAGE,
MAGNITUDE_POWER_ACTIVE,
MAGNITUDE_ENERGY
};
for (const auto& type : types) {
for (size_t index = 0; index < sensor_magnitude_t::counts(type); ++index) {
delSetting(_magnitudeSettingsRatioKey(type, index));
}
}
for (auto& ptr : _sensors) {
if (_sensorIsEmon(ptr)) {
DEBUG_MSG_P(PSTR("[EMON] Resetting %s\n"), ptr->description().c_str());
static_cast<BaseEmonSensor*>(ptr)->resetRatios();
}
}
}
double _sensorApiEmonExpectedValue(const sensor_magnitude_t& magnitude, double expected) {
if (!_sensorIsEmon(magnitude.sensor)) {
return BaseEmonSensor::DefaultRatio;
}
auto* sensor = static_cast<BaseEmonSensor*>(magnitude.sensor);
return sensor->ratioFromValue(magnitude.slot, sensor->value(magnitude.slot), expected);
}
} // namespace
// -----------------------------------------------------------------------------
// WebUI Websockets API
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
namespace {
bool _sensorWebSocketOnKeyCheck(const char* key, JsonVariant&) {
return _sensorCheckKeyPrefix(key);
}
String _sensorError(unsigned char error) {
const __FlashStringHelper* result = nullptr;
switch (error) {
case SENSOR_ERROR_OK:
result = F("OK");
break;
case SENSOR_ERROR_OUT_OF_RANGE:
result = F("Out of Range");
break;
case SENSOR_ERROR_WARM_UP:
result = F("Warming Up");
break;
case SENSOR_ERROR_TIMEOUT:
result = F("Timeout");
break;
case SENSOR_ERROR_UNKNOWN_ID:
result = F("Unknown ID");
break;
case SENSOR_ERROR_CRC:
result = F("CRC / Data Error");
break;
case SENSOR_ERROR_I2C:
result = F("I2C Error");
break;
case SENSOR_ERROR_GPIO_USED:
result = F("GPIO Already Used");
break;
case SENSOR_ERROR_CALIBRATION:
result = F("Calibration Error");
break;
default:
case SENSOR_ERROR_OTHER:
result = F("Other / Unknown Error");
break;
}
return result;
}
String _magnitudeName(unsigned char type) {
const __FlashStringHelper* result = nullptr;
switch (type) {
case MAGNITUDE_TEMPERATURE:
result = F("Temperature");
break;
case MAGNITUDE_HUMIDITY:
result = F("Humidity");
break;
case MAGNITUDE_PRESSURE:
result = F("Pressure");
break;
case MAGNITUDE_CURRENT:
result = F("Current");
break;
case MAGNITUDE_VOLTAGE:
result = F("Voltage");
break;
case MAGNITUDE_POWER_ACTIVE:
result = F("Active Power");
break;
case MAGNITUDE_POWER_APPARENT:
result = F("Apparent Power");
break;
case MAGNITUDE_POWER_REACTIVE:
result = F("Reactive Power");
break;
case MAGNITUDE_POWER_FACTOR:
result = F("Power Factor");
break;
case MAGNITUDE_ENERGY:
result = F("Energy");
break;
case MAGNITUDE_ENERGY_DELTA:
result = F("Energy (delta)");
break;
case MAGNITUDE_ANALOG:
result = F("Analog");
break;
case MAGNITUDE_DIGITAL:
result = F("Digital");
break;
case MAGNITUDE_EVENT:
result = F("Event");
break;
case MAGNITUDE_PM1DOT0:
result = F("PM1.0");
break;
case MAGNITUDE_PM2DOT5:
result = F("PM2.5");
break;
case MAGNITUDE_PM10:
result = F("PM10");
break;
case MAGNITUDE_CO2:
result = F("CO2");
break;
case MAGNITUDE_VOC:
result = F("VOC");
break;
case MAGNITUDE_IAQ_STATIC:
result = F("IAQ (Static)");
break;
case MAGNITUDE_IAQ:
result = F("IAQ");
break;
case MAGNITUDE_IAQ_ACCURACY:
result = F("IAQ Accuracy");
break;
case MAGNITUDE_LUX:
result = F("Lux");
break;
case MAGNITUDE_UVA:
result = F("UVA");
break;
case MAGNITUDE_UVB:
result = F("UVB");
break;
case MAGNITUDE_UVI:
result = F("UVI");
break;
case MAGNITUDE_DISTANCE:
result = F("Distance");
break;
case MAGNITUDE_HCHO:
result = F("HCHO");
break;
case MAGNITUDE_GEIGER_CPM:
case MAGNITUDE_GEIGER_SIEVERT:
result = F("Local Dose Rate");
break;
case MAGNITUDE_COUNT:
result = F("Count");
break;
case MAGNITUDE_NO2:
result = F("NO2");
break;
case MAGNITUDE_CO:
result = F("CO");
break;
case MAGNITUDE_RESISTANCE:
result = F("Resistance");
break;
case MAGNITUDE_PH:
result = F("pH");
break;
case MAGNITUDE_FREQUENCY:
result = F("Frequency");
break;
case MAGNITUDE_TVOC:
result = F("TVOC");
break;
case MAGNITUDE_CH2O:
result = F("CH2O");
break;
case MAGNITUDE_NONE:
default:
break;
}
return String(result);
}
// prepare available types and magnitudes config
// make sure these are properly ordered, as UI does not delay processing
void _sensorWebSocketTypes(JsonObject& root) {
::web::ws::EnumerablePayload payload{root, STRING_VIEW("types")};
payload(STRING_VIEW("values"), {MAGNITUDE_NONE + 1, MAGNITUDE_MAX},
[](size_t type) {
return sensor_magnitude_t::counts(type) > 0;
},
{
{STRING_VIEW("type"), [](JsonArray& out, size_t index) {
out.add(index);
}},
{STRING_VIEW("prefix"), [](JsonArray& out, size_t index) {
out.add(_magnitudeSettingsPrefix(index));
}},
{STRING_VIEW("name"), [](JsonArray& out, size_t index) {
out.add(_magnitudeName(index));
}}
});
}
void _sensorWebSocketErrors(JsonObject& root) {
::web::ws::EnumerablePayload payload{root, STRING_VIEW("errors")};
payload(STRING_VIEW("values"), SENSOR_ERROR_MAX, {
{STRING_VIEW("type"), [](JsonArray& out, size_t index) {
out.add(index);
}},
{STRING_VIEW("name"), [](JsonArray& out, size_t index) {
out.add(_sensorError(index));
}}
});
}
void _sensorWebSocketUnits(JsonObject& root) {
::web::ws::EnumerablePayload payload{root, STRING_VIEW("units")};
payload(STRING_VIEW("values"), _magnitudes.size(), {
{STRING_VIEW("supported"), [](JsonArray& out, size_t index) {
JsonArray& units = out.createNestedArray();
const auto range = _magnitudeUnitsRange(_magnitudes[index].type);
for (auto it = range.begin(); it != range.end(); ++it) {
JsonArray& unit = units.createNestedArray();
unit.add(static_cast<int>(*it));
unit.add(_magnitudeUnits(*it));
}
}}
});
}
void _sensorWebSocketList(JsonObject& root) {
::web::ws::EnumerablePayload payload{root, STRING_VIEW("magnitudes-list")};
payload(STRING_VIEW("values"), _magnitudes.size(), {
{STRING_VIEW("index_global"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].index_global);
}},
{STRING_VIEW("type"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].type);
}},
{STRING_VIEW("description"), [](JsonArray& out, size_t index) {
out.add(_magnitudeDescription(_magnitudes[index]));
}},
{STRING_VIEW("units"), [](JsonArray& out, size_t index) {
out.add(static_cast<int>(_magnitudes[index].units));
}}
});
}
void _sensorWebSocketSettings(JsonObject& root) {
// XXX: inject 'null' in the output. need this for optional fields, since the current
// version of serializer only does this for char ptr and even makes NaN serialized as
// NaN, instead of more commonly used null (but, expect this to be fixed after switching to v6+)
static const char* const NullSymbol { nullptr };
::web::ws::EnumerablePayload payload{root, STRING_VIEW("magnitudes-settings")};
payload(STRING_VIEW("values"), _magnitudes.size(), {
{STRING_VIEW("Correction"), [](JsonArray& out, size_t index) {
const auto& magnitude = _magnitudes[index];
if (_magnitudeCorrectionSupported(magnitude.type)) {
out.add(magnitude.correction);
} else {
out.add(NullSymbol);
}
}},
{STRING_VIEW("Ratio"), [](JsonArray& out, size_t index) {
const auto& magnitude = _magnitudes[index];
if (_magnitudeRatioSupported(magnitude.type)) {
out.add(static_cast<BaseEmonSensor*>(magnitude.sensor)->getRatio(magnitude.slot));
} else {
out.add(NullSymbol);
}
}},
{STRING_VIEW("ZeroThreshold"), [](JsonArray& out, size_t index) {
const auto threshold = _magnitudes[index].zero_threshold;
if (!std::isnan(threshold)) {
out.add(threshold);
} else {
out.add(NullSymbol);
}
}},
{STRING_VIEW("MinDelta"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].min_delta);
}},
{STRING_VIEW("MaxDelta"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].max_delta);
}}
});
root["snsRead"] = _sensor_read_interval.count();
root["snsInit"] = _sensor_init_interval.count();
root["snsSave"] = _sensor_energy_tracker.every();
root["snsReport"] = _sensor_report_every;
root["snsRealTime"] = _sensor_real_time;
}
void _sensorWebSocketSendData(JsonObject& root) {
if (_magnitudes.size()) {
::web::ws::EnumerablePayload payload{root, STRING_VIEW("magnitudes")};
payload(STRING_VIEW("values"), _magnitudes.size(), {
{STRING_VIEW("value"), [](JsonArray& out, size_t index) {
char buffer[64];
dtostrf(_magnitudeProcess(
_magnitudes[index], _magnitudes[index].last),
1, _magnitudes[index].decimals, buffer);
out.add(buffer);
}},
{STRING_VIEW("error"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].sensor->error());
}},
{STRING_VIEW("info"), [](JsonArray& out, size_t index) {
#if NTP_SUPPORT
if ((_magnitudes[index].type == MAGNITUDE_ENERGY) && (_sensor_energy_tracker)) {
out.add(String(F("Last saved: "))
+ getSetting({"eneTime", _magnitudes[index].index_global},
F("(unknown)")));
} else {
#endif
out.add("");
#if NTP_SUPPORT
}
#endif
}}
});
}
}
void _sensorWebSocketOnAction(uint32_t client_id, const char* action, JsonObject& data) {
if (strcmp(action, "emon-expected") == 0) {
auto id = data["id"].as<size_t>();
if (id < _magnitudes.size()) {
auto expected = data["expected"].as<float>();
wsPost(client_id, [id, expected](JsonObject& root) {
const auto& magnitude = _magnitudes[id];
String key { F("result:") };
key += _magnitudeSettingsRatioKey(magnitude).value();
root[key] = _sensorApiEmonExpectedValue(magnitude, expected);
});
}
} else if (strcmp(action, "emon-reset-ratios") == 0) {
_sensorApiEmonResetRatios();
} else if (strcmp(action, "analog-calibrate") == 0) {
_sensorApiAnalogCalibrate();
}
}
void _sensorWebSocketOnVisible(JsonObject& root) {
wsPayloadModule(root, "sns");
for (auto* sensor [[gnu::unused]] : _sensors) {
if (_sensorIsEmon(sensor)) {
wsPayloadModule(root, "emon");
}
switch (sensor->getID()) {
#if HLW8012_SUPPORT
case SENSOR_HLW8012_ID:
wsPayloadModule(root, "hlw");
break;
#endif
#if CSE7766_SUPPORT
case SENSOR_CSE7766_ID:
wsPayloadModule(root, "cse");
break;
#endif
#if PZEM004T_SUPPORT || PZEM004TV30_SUPPORT
case SENSOR_PZEM004T_ID:
case SENSOR_PZEM004TV30_ID:
wsPayloadModule(root, "pzem");
break;
#endif
#if PULSEMETER_SUPPORT
case SENSOR_PULSEMETER_ID:
wsPayloadModule(root, "pm");
break;
#endif
#if MICS2710_SUPPORT || MICS5525_SUPPORT
case SENSOR_MICS2710_ID:
case SENSOR_MICS5525_ID:
wsPayloadModule(root, "mics");
break;
#endif
}
}
}
// Entries related to things reported by the module.
// - types of magnitudes that are available and the string values associated with them
// - error types and stringified versions of them
// - units are the value types of the magnitude
// TODO: magnitude types have some common keys and some specific ones, only implemented for the type
// e.g. voltMains is specific to the MAGNITUDE_VOLTAGE but *only* in analog mode, or eneRatio specific to MAGNITUDE_ENERGY
// but, notice that the sensor will probably be used to 'get' certain properties, to generate certain keys list
// TODO: report common keys either here or in the data payload
// some preprocessor magic might need to happen though, as prefixes are retrieved via `_magnitudeSettingsPrefix(type)`
// (also there is c++17 where string_view and char arrays may be concatenated at compile time)
void _sensorWebSocketOnConnectedInitial(JsonObject& root) {
if (!_magnitudes.size()) {
return;
}
JsonObject& container = root.createNestedObject(F("magnitudes-init"));
_sensorWebSocketTypes(container);
_sensorWebSocketErrors(container);
_sensorWebSocketUnits(container);
}
// Entries specific to the sensor_magnitude_t; type, info, description
void _sensorWebSocketOnConnectedList(JsonObject& root) {
if (!_magnitudes.size()) {
return;
}
_sensorWebSocketList(root);
}
void _sensorWebSocketOnConnectedSettings(JsonObject& root) {
if (!_magnitudes.size()) {
return;
}
_sensorWebSocketSettings(root);
}
} // namespace
// Used by modules to generate magnitude_id<->module_id mapping for the WebUI
// Prefix controls the UI templates, supplied callback should retrieve module-specific value Id
void sensorWebSocketMagnitudes(JsonObject& root, const char* prefix, SensorWebSocketMagnitudesCallback callback) {
::web::ws::EnumerablePayload payload{root, STRING_VIEW("magnitudes-module")};
auto& container = payload.root();
container[F("prefix")] = prefix;
payload(STRING_VIEW("values"), _magnitudes.size(), {
{STRING_VIEW("type"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].type);
}},
{STRING_VIEW("index_global"), [](JsonArray& out, size_t index) {
out.add(_magnitudes[index].index_global);
}},
{STRING_VIEW("index_module"), callback}
});
}
#endif // WEB_SUPPORT
#if API_SUPPORT
namespace {
String _sensorApiMagnitudeName(sensor_magnitude_t& magnitude) {
String name = _magnitudeTopic(magnitude.type);
if (SENSOR_USE_INDEX || (sensor_magnitude_t::counts(magnitude.type) > 1)) name = name + "/" + String(magnitude.index_global);
return name;
}
bool _sensorApiTryParseMagnitudeIndex(const char* p, unsigned char type, unsigned char& magnitude_index) {
char* endp { nullptr };
const unsigned long result { strtoul(p, &endp, 10) };
if ((endp == p) || (*endp != '\0') || (result >= sensor_magnitude_t::counts(type))) {
DEBUG_MSG_P(PSTR("[SENSOR] Invalid magnitude ID (%s)\n"), p);
return false;
}
magnitude_index = result;
return true;
}
template <typename T>
bool _sensorApiTryHandle(ApiRequest& request, unsigned char type, T&& callback) {
unsigned char index { 0u };
if (request.wildcards()) {
auto index_param = request.wildcard(0);
if (!_sensorApiTryParseMagnitudeIndex(index_param.c_str(), type, index)) {
return false;
}
}
for (auto& magnitude : _magnitudes) {
if ((type == magnitude.type) && (index == magnitude.index_global)) {
callback(magnitude);
return true;
}
}
return false;
}
void _sensorApiSetup() {
apiRegister(F("magnitudes"),
[](ApiRequest&, JsonObject& root) {
JsonArray& magnitudes = root.createNestedArray("magnitudes");
for (auto& magnitude : _magnitudes) {
JsonArray& data = magnitudes.createNestedArray();
data.add(_sensorApiMagnitudeName(magnitude));
data.add(magnitude.last);
data.add(magnitude.reported);
}
return true;
},
nullptr
);
_magnitudeForEachCounted([](unsigned char type) {
String pattern = _magnitudeTopic(type);
if (SENSOR_USE_INDEX || (sensor_magnitude_t::counts(type) > 1)) {
pattern += "/+";
}
ApiBasicHandler get {
[type](ApiRequest& request) {
return _sensorApiTryHandle(request, type, [&](const sensor_magnitude_t& magnitude) {
char buffer[64] { 0 };
dtostrf(
_sensor_real_time ? magnitude.last : magnitude.reported,
1, magnitude.decimals,
buffer
);
request.send(String(buffer));
return true;
});
}
};
ApiBasicHandler put { nullptr };
if (type == MAGNITUDE_ENERGY) {
put = [](ApiRequest& request) {
return _sensorApiTryHandle(request, MAGNITUDE_ENERGY, [&](const sensor_magnitude_t& magnitude) {
_sensorApiResetEnergy(magnitude, request.param(F("value")));
});
};
}
apiRegister(pattern, std::move(get), std::move(put));
});
}
} // namespace
#endif // API_SUPPORT == 1
#if MQTT_SUPPORT
namespace {
void _sensorMqttCallback(unsigned int type, const char* topic, char* payload) {
static const auto energy_topic = _magnitudeTopic(MAGNITUDE_ENERGY);
switch (type) {
case MQTT_MESSAGE_EVENT: {
String t = mqttMagnitude(topic);
if (!t.startsWith(energy_topic)) break;
unsigned int index = t.substring(energy_topic.length() + 1).toInt();
if (index >= sensor_magnitude_t::counts(MAGNITUDE_ENERGY)) break;
for (auto& magnitude : _magnitudes) {
if (MAGNITUDE_ENERGY != magnitude.type) continue;
if (index != magnitude.index_global) continue;
_sensorApiResetEnergy(magnitude, static_cast<const char*>(payload));
break;
}
break;
}
case MQTT_CONNECT_EVENT: {
for (auto& magnitude : _magnitudes) {
if (MAGNITUDE_ENERGY == magnitude.type) {
const String topic = energy_topic + "/+";
mqttSubscribe(topic.c_str());
break;
}
}
break;
}
case MQTT_DISCONNECT_EVENT:
break;
}
}
} // namespace
#endif // MQTT_SUPPORT == 1
#if TERMINAL_SUPPORT
namespace {
void _sensorInitCommands() {
terminalRegisterCommand(F("MAGNITUDES"), [](::terminal::CommandContext&& ctx) {
char last[64];
char reported[64];
for (size_t index = 0; index < _magnitudes.size(); ++index) {
auto& magnitude = _magnitudes.at(index);
dtostrf(magnitude.last, 1, magnitude.decimals, last);
dtostrf(magnitude.reported, 1, magnitude.decimals, reported);
ctx.output.printf_P(PSTR("%u * %s/%u @ %s (read:%s reported:%s units:%s)\n"),
index, _magnitudeTopic(magnitude.type).c_str(), magnitude.index_global,
_magnitudeDescription(magnitude).c_str(), last, reported,
_magnitudeUnits(magnitude).c_str());
}
terminalOK();
});
terminalRegisterCommand(F("EXPECTED"), [](::terminal::CommandContext&& ctx) {
if (ctx.argv.size() == 3) {
const auto id = settings::internal::convert<size_t>(ctx.argv[1]);
if (id < _magnitudes.size()) {
const auto result = _sensorApiEmonExpectedValue(_magnitudes[id],
settings::internal::convert<double>(ctx.argv[2]));
const auto key = _magnitudeSettingsRatioKey(_magnitudes[id]);
ctx.output.printf("%s => %s\n", key.c_str(), String(result).c_str());
terminalOK(ctx);
return;
}
terminalError(ctx, F("Invalid magnitude ID"));
return;
}
terminalError(ctx, F("EXPECTED <ID> <VALUE>"));
});
terminalRegisterCommand(F("RESET.RATIOS"), [](::terminal::CommandContext&& ctx) {
_sensorApiEmonResetRatios();
terminalOK(ctx);
});
terminalRegisterCommand(F("ENERGY"), [](::terminal::CommandContext&& ctx) {
using IndexType = decltype(sensor_magnitude_t::index_global);
if (ctx.argv.size() == 3) {
const auto selected = settings::internal::convert<IndexType>(ctx.argv[1]);
const auto energy = _sensorParseEnergy(ctx.argv[2]);
if (!energy) {
terminalError(ctx, F("Invalid energy string"));
return;
}
for (auto& magnitude : _magnitudes) {
if ((MAGNITUDE_ENERGY == magnitude.type) && (selected == magnitude.index_global) && _sensorIsEmon(magnitude.sensor)) {
static_cast<BaseEmonSensor*>(magnitude.sensor)->resetEnergy(magnitude.slot, energy.value());
terminalOK(ctx);
return;
}
}
terminalError(ctx, F("Magnitude not found"));
return;
}
terminalError(ctx, F("ENERGY <ID> <VALUE>"));
});
}
} // namespace
#endif // TERMINAL_SUPPORT == 1
namespace {
void _sensorTick() {
for (auto* sensor : _sensors) {
sensor->tick();
}
}
void _sensorPre() {
for (auto* sensor : _sensors) {
sensor->pre();
if (!sensor->status()) {
DEBUG_MSG_P(PSTR("[SENSOR] Error reading data from %s (error: %d)\n"),
sensor->description().c_str(),
sensor->error()
);
}
}
}
void _sensorPost() {
for (auto* sensor : _sensors) {
sensor->post();
}
}
} // namespace
// -----------------------------------------------------------------------------
// Sensor initialization
// -----------------------------------------------------------------------------
namespace {
void _sensorLoad() {
/*
This is temporal, in the future sensors will be initialized based on
soft configuration (data stored in EEPROM config) so you will be able
to define and configure new sensors on the fly
At the time being, only enabled sensors (those with *_SUPPORT to 1) are being
loaded and initialized here. If you want to add new sensors of the same type
just duplicate the block and change the arguments for the set* methods.
For example, how to add a second DHT sensor:
#if DHT_SUPPORT
{
DHTSensor * sensor = new DHTSensor();
sensor->setGPIO(DHT2_PIN);
sensor->setType(DHT2_TYPE);
_sensors.push_back(sensor);
}
#endif
DHT2_PIN and DHT2_TYPE should be globally accessible:
- as `build_src_flags = -DDHT2_PIN=... -DDHT2_TYPE=...`
- in custom.h, as `#define ...`
*/
#if AM2320_SUPPORT
{
AM2320Sensor * sensor = new AM2320Sensor();
sensor->setAddress(AM2320_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if ANALOG_SUPPORT
{
AnalogSensor * sensor = new AnalogSensor();
sensor->setSamples(ANALOG_SAMPLES);
sensor->setDelay(ANALOG_DELAY);
//CICM For analog scaling
sensor->setFactor(ANALOG_FACTOR);
sensor->setOffset(ANALOG_OFFSET);
_sensors.push_back(sensor);
}
#endif
#if BH1750_SUPPORT
{
BH1750Sensor * sensor = new BH1750Sensor();
sensor->setAddress(BH1750_ADDRESS);
sensor->setMode(BH1750_MODE);
_sensors.push_back(sensor);
}
#endif
#if BMP180_SUPPORT
{
BMP180Sensor * sensor = new BMP180Sensor();
sensor->setAddress(BMP180_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if BMX280_SUPPORT
{
// Support up to two sensors with full auto-discovery.
const unsigned char number = constrain(getSetting("bmx280Number", BMX280_NUMBER), 1, 2);
// For second sensor, if BMX280_ADDRESS is 0x00 then auto-discover
// otherwise choose the other unnamed sensor address
const auto first = getSetting("bmx280Address", BMX280_ADDRESS);
const auto second = (first == 0x00) ? 0x00 : (0x76 + 0x77 - first);
const decltype(first) address_map[2] { first, second };
for (unsigned char n=0; n < number; ++n) {
BMX280Sensor * sensor = new BMX280Sensor();
sensor->setAddress(address_map[n]);
_sensors.push_back(sensor);
}
}
#endif
#if BME680_SUPPORT
{
BME680Sensor * sensor = new BME680Sensor();
sensor->setAddress(BME680_I2C_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if CSE7766_SUPPORT
{
CSE7766Sensor * sensor = new CSE7766Sensor();
sensor->setRX(CSE7766_RX_PIN);
_sensors.push_back(sensor);
}
#endif
#if DALLAS_SUPPORT
{
DallasSensor * sensor = new DallasSensor();
sensor->setGPIO(DALLAS_PIN);
_sensors.push_back(sensor);
}
#endif
#if DHT_SUPPORT
{
DHTSensor * sensor = new DHTSensor();
sensor->setGPIO(DHT_PIN);
sensor->setType(DHT_TYPE);
_sensors.push_back(sensor);
}
#endif
#if DIGITAL_SUPPORT
{
auto getPin = [](unsigned char index) -> int {
switch (index) {
case 0: return DIGITAL1_PIN;
case 1: return DIGITAL2_PIN;
case 2: return DIGITAL3_PIN;
case 3: return DIGITAL4_PIN;
case 4: return DIGITAL5_PIN;
case 5: return DIGITAL6_PIN;
case 6: return DIGITAL7_PIN;
case 7: return DIGITAL8_PIN;
default: return GPIO_NONE;
}
};
auto getDefaultState = [](unsigned char index) -> int {
switch (index) {
case 0: return DIGITAL1_DEFAULT_STATE;
case 1: return DIGITAL2_DEFAULT_STATE;
case 2: return DIGITAL3_DEFAULT_STATE;
case 3: return DIGITAL4_DEFAULT_STATE;
case 4: return DIGITAL5_DEFAULT_STATE;
case 5: return DIGITAL6_DEFAULT_STATE;
case 6: return DIGITAL7_DEFAULT_STATE;
case 7: return DIGITAL8_DEFAULT_STATE;
default: return 1;
}
};
auto getMode = [](unsigned char index) -> int {
switch (index) {
case 0: return DIGITAL1_PIN_MODE;
case 1: return DIGITAL2_PIN_MODE;
case 2: return DIGITAL3_PIN_MODE;
case 3: return DIGITAL4_PIN_MODE;
case 4: return DIGITAL5_PIN_MODE;
case 5: return DIGITAL6_PIN_MODE;
case 6: return DIGITAL7_PIN_MODE;
case 7: return DIGITAL8_PIN_MODE;
default: return INPUT_PULLUP;
}
};
auto pins = gpioPins();
for (unsigned char index = 0; index < pins; ++index) {
const auto pin = getPin(index);
if (pin == GPIO_NONE) break;
DigitalSensor * sensor = new DigitalSensor();
sensor->setGPIO(pin);
sensor->setMode(getMode(index));
sensor->setDefault(getDefaultState(index));
_sensors.push_back(sensor);
}
}
#endif
#if ECH1560_SUPPORT
{
ECH1560Sensor * sensor = new ECH1560Sensor();
sensor->setCLK(ECH1560_CLK_PIN);
sensor->setMISO(ECH1560_MISO_PIN);
sensor->setInverted(ECH1560_INVERTED);
_sensors.push_back(sensor);
}
#endif
#if EMON_ADC121_SUPPORT
{
EmonADC121Sensor * sensor = new EmonADC121Sensor();
sensor->setAddress(EMON_ADC121_I2C_ADDRESS);
sensor->setVoltage(EMON_MAINS_VOLTAGE);
sensor->setReferenceVoltage(EMON_REFERENCE_VOLTAGE);
_sensors.push_back(sensor);
}
#endif
#if EMON_ADS1X15_SUPPORT
{
auto port = std::make_shared<EmonADS1X15Sensor::I2CPort>(
EMON_ADS1X15_I2C_ADDRESS, EMON_ADS1X15_TYPE, EMON_ADS1X15_GAIN, EMON_ADS1X15_DATARATE);
constexpr unsigned char FirstBit { 1 };
unsigned char mask { EMON_ADS1X15_MASK };
unsigned char channel { 0 };
while (mask) {
if (mask & FirstBit) {
auto* sensor = new EmonADS1X15Sensor(port);
sensor->setVoltage(EMON_MAINS_VOLTAGE);
sensor->setChannel(channel);
_sensors.push_back(sensor);
}
++channel;
mask >>= 1;
}
}
#endif
#if EMON_ANALOG_SUPPORT
{
auto* sensor = new EmonAnalogSensor();
sensor->setVoltage(EMON_MAINS_VOLTAGE);
sensor->setReferenceVoltage(EMON_REFERENCE_VOLTAGE);
sensor->setResolution(EMON_ANALOG_RESOLUTION);
_sensors.push_back(sensor);
}
#endif
#if EVENTS_SUPPORT
{
auto getPin = [](unsigned char index) -> int {
switch (index) {
case 0: return EVENTS1_PIN;
case 1: return EVENTS2_PIN;
case 2: return EVENTS3_PIN;
case 3: return EVENTS4_PIN;
case 4: return EVENTS5_PIN;
case 5: return EVENTS6_PIN;
case 6: return EVENTS7_PIN;
case 7: return EVENTS8_PIN;
default: return GPIO_NONE;
}
};
auto getMode = [](unsigned char index) -> int {
switch (index) {
case 0: return EVENTS1_PIN_MODE;
case 1: return EVENTS2_PIN_MODE;
case 2: return EVENTS3_PIN_MODE;
case 3: return EVENTS4_PIN_MODE;
case 4: return EVENTS5_PIN_MODE;
case 5: return EVENTS6_PIN_MODE;
case 6: return EVENTS7_PIN_MODE;
case 7: return EVENTS8_PIN_MODE;
default: return INPUT;
}
};
auto getDebounce = [](unsigned char index) -> unsigned long {
switch (index) {
case 0: return EVENTS1_DEBOUNCE;
case 1: return EVENTS2_DEBOUNCE;
case 2: return EVENTS3_DEBOUNCE;
case 3: return EVENTS4_DEBOUNCE;
case 4: return EVENTS5_DEBOUNCE;
case 5: return EVENTS6_DEBOUNCE;
case 6: return EVENTS7_DEBOUNCE;
case 7: return EVENTS8_DEBOUNCE;
default: return 50;
}
};
auto getIsrMode = [](unsigned char index) -> int {
switch (index) {
case 0: return EVENTS1_INTERRUPT_MODE;
case 1: return EVENTS2_INTERRUPT_MODE;
case 2: return EVENTS3_INTERRUPT_MODE;
case 3: return EVENTS4_INTERRUPT_MODE;
case 4: return EVENTS5_INTERRUPT_MODE;
case 5: return EVENTS6_INTERRUPT_MODE;
case 6: return EVENTS7_INTERRUPT_MODE;
case 7: return EVENTS8_INTERRUPT_MODE;
default: return RISING;
}
};
auto pins = gpioPins();
for (unsigned char index = 0; index < pins; ++index) {
const auto pin = getPin(index);
if (pin == GPIO_NONE) break;
EventSensor * sensor = new EventSensor();
sensor->setGPIO(pin);
sensor->setPinMode(getMode(index));
sensor->setDebounceTime(getDebounce(index));
sensor->setInterruptMode(getIsrMode(index));
_sensors.push_back(sensor);
}
}
#endif
#if GEIGER_SUPPORT
{
GeigerSensor * sensor = new GeigerSensor(); // Create instance of thr Geiger module.
sensor->setGPIO(GEIGER_PIN); // Interrupt pin of the attached geiger counter board.
sensor->setMode(GEIGER_PIN_MODE); // This pin is an input.
sensor->setDebounceTime(GEIGER_DEBOUNCE); // Debounce time 25ms, because https://github.com/Trickx/espurna/wiki/Geiger-counter
sensor->setInterruptMode(GEIGER_INTERRUPT_MODE); // Interrupt triggering: edge detection rising.
sensor->setCPM2SievertFactor(GEIGER_CPM2SIEVERT); // Conversion factor from counts per minute to µSv/h
_sensors.push_back(sensor);
}
#endif
#if GUVAS12SD_SUPPORT
{
GUVAS12SDSensor * sensor = new GUVAS12SDSensor();
sensor->setGPIO(GUVAS12SD_PIN);
_sensors.push_back(sensor);
}
#endif
#if SONAR_SUPPORT
{
SonarSensor * sensor = new SonarSensor();
sensor->setEcho(SONAR_ECHO);
sensor->setIterations(SONAR_ITERATIONS);
sensor->setMaxDistance(SONAR_MAX_DISTANCE);
sensor->setTrigger(SONAR_TRIGGER);
_sensors.push_back(sensor);
}
#endif
#if HLW8012_SUPPORT
{
HLW8012Sensor * sensor = new HLW8012Sensor();
sensor->setSEL(getSetting(F("hlw8012SEL"), HLW8012_SEL_PIN));
sensor->setCF(getSetting(F("hlw8012CF"), HLW8012_CF_PIN));
sensor->setCF1(getSetting(F("hlw8012CF1"), HLW8012_CF1_PIN));
sensor->setSELCurrent(HLW8012_SEL_CURRENT);
_sensors.push_back(sensor);
}
#endif
#if LDR_SUPPORT
{
LDRSensor * sensor = new LDRSensor();
sensor->setSamples(LDR_SAMPLES);
sensor->setDelay(LDR_DELAY);
sensor->setType(LDR_TYPE);
sensor->setPhotocellPositionOnGround(LDR_ON_GROUND);
sensor->setResistor(LDR_RESISTOR);
sensor->setPhotocellParameters(LDR_MULTIPLICATION, LDR_POWER);
_sensors.push_back(sensor);
}
#endif
#if MHZ19_SUPPORT
{
MHZ19Sensor * sensor = new MHZ19Sensor();
sensor->setRX(MHZ19_RX_PIN);
sensor->setTX(MHZ19_TX_PIN);
sensor->setCalibrateAuto(getSetting("mhz19CalibrateAuto", false));
_sensors.push_back(sensor);
}
#endif
#if MICS2710_SUPPORT
{
MICS2710Sensor * sensor = new MICS2710Sensor();
sensor->setAnalogGPIO(MICS2710_NOX_PIN);
sensor->setPreHeatGPIO(MICS2710_PRE_PIN);
sensor->setR0(MICS2710_R0);
sensor->setRL(MICS2710_RL);
sensor->setRS(0);
_sensors.push_back(sensor);
}
#endif
#if MICS5525_SUPPORT
{
MICS5525Sensor * sensor = new MICS5525Sensor();
sensor->setAnalogGPIO(MICS5525_RED_PIN);
sensor->setR0(MICS5525_R0);
sensor->setRL(MICS5525_RL);
sensor->setRS(0);
_sensors.push_back(sensor);
}
#endif
#if NTC_SUPPORT
{
NTCSensor * sensor = new NTCSensor();
sensor->setSamples(NTC_SAMPLES);
sensor->setDelay(NTC_DELAY);
sensor->setUpstreamResistor(NTC_R_UP);
sensor->setDownstreamResistor(NTC_R_DOWN);
sensor->setBeta(NTC_BETA);
sensor->setR0(NTC_R0);
sensor->setT0(NTC_T0);
_sensors.push_back(sensor);
}
#endif
#if PMSX003_SUPPORT
{
PMSX003Sensor * sensor = new PMSX003Sensor();
#if PMS_USE_SOFT
sensor->setRX(PMS_RX_PIN);
sensor->setTX(PMS_TX_PIN);
#else
sensor->setSerial(& PMS_HW_PORT);
#endif
sensor->setType(PMS_TYPE);
_sensors.push_back(sensor);
}
#endif
#if PULSEMETER_SUPPORT
{
PulseMeterSensor * sensor = new PulseMeterSensor();
sensor->setGPIO(PULSEMETER_PIN);
sensor->setInterruptMode(PULSEMETER_INTERRUPT_ON);
sensor->setDebounceTime(PULSEMETER_DEBOUNCE);
_sensors.push_back(sensor);
}
#endif
#if PZEM004T_SUPPORT
{
PZEM004TSensor::PortPtr port;
auto rx = getSetting("pzemRX", PZEM004TSensor::RxPin);
auto tx = getSetting("pzemTX", PZEM004TSensor::TxPin);
if (getSetting("pzemSoft", PZEM004TSensor::useSoftwareSerial())) {
port = PZEM004TSensor::makeSoftwarePort(rx, tx);
} else {
port = PZEM004TSensor::makeHardwarePort(
PZEM004TSensor::defaultHardwarePort(), rx, tx);
}
if (!port) {
return;
}
bool initialized { false };
#if !defined(PZEM004T_ADDRESSES)
for (size_t index = 0; index < PZEM004TSensor::DevicesMax; ++index) {
auto address = getSetting({"pzemAddr", index}, PZEM004TSensor::defaultAddress(index));
if (!address.isSet()) {
break;
}
auto* ptr = PZEM004TSensor::make(port, address);
if (ptr) {
_sensors.push_back(ptr);
initialized = true;
}
}
#else
String addrs = getSetting("pzemAddr", F(PZEM004T_ADDRESSES));
constexpr size_t BufferSize{64};
char buffer[BufferSize]{0};
if (addrs.length() < BufferSize) {
std::copy(addrs.c_str(), addrs.c_str() + addrs.length(), buffer);
buffer[addrs.length()] = '\0';
size_t device{0};
char* address{strtok(buffer, " ")};
while ((device < PZEM004TSensor::DevicesMax) && (address != nullptr)) {
auto* ptr = PZEM004TSensor::make(port, address);
if (ptr) {
_sensors.push_back(ptr);
initialized = true;
}
}
}
#endif
if (initialized) {
PZEM004TSensor::registerTerminalCommands();
}
}
#endif
#if SENSEAIR_SUPPORT
{
SenseAirSensor * sensor = new SenseAirSensor();
sensor->setRX(SENSEAIR_RX_PIN);
sensor->setTX(SENSEAIR_TX_PIN);
_sensors.push_back(sensor);
}
#endif
#if SDS011_SUPPORT
{
SDS011Sensor * sensor = new SDS011Sensor();
sensor->setRX(SDS011_RX_PIN);
sensor->setTX(SDS011_TX_PIN);
_sensors.push_back(sensor);
}
#endif
#if SHT3X_I2C_SUPPORT
{
SHT3XI2CSensor * sensor = new SHT3XI2CSensor();
sensor->setAddress(SHT3X_I2C_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if SI7021_SUPPORT
{
SI7021Sensor * sensor = new SI7021Sensor();
sensor->setAddress(SI7021_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if SM300D2_SUPPORT
{
SM300D2Sensor * sensor = new SM300D2Sensor();
sensor->setRX(SM300D2_RX_PIN);
_sensors.push_back(sensor);
}
#endif
#if T6613_SUPPORT
{
T6613Sensor * sensor = new T6613Sensor();
sensor->setRX(T6613_RX_PIN);
sensor->setTX(T6613_TX_PIN);
_sensors.push_back(sensor);
}
#endif
#if TMP3X_SUPPORT
{
TMP3XSensor * sensor = new TMP3XSensor();
sensor->setType(TMP3X_TYPE);
_sensors.push_back(sensor);
}
#endif
#if V9261F_SUPPORT
{
V9261FSensor * sensor = new V9261FSensor();
sensor->setRX(V9261F_PIN);
sensor->setInverted(V9261F_PIN_INVERSE);
_sensors.push_back(sensor);
}
#endif
#if MAX6675_SUPPORT
{
MAX6675Sensor * sensor = new MAX6675Sensor();
sensor->setCS(MAX6675_CS_PIN);
sensor->setSO(MAX6675_SO_PIN);
sensor->setSCK(MAX6675_SCK_PIN);
_sensors.push_back(sensor);
}
#endif
#if VEML6075_SUPPORT
{
VEML6075Sensor * sensor = new VEML6075Sensor();
sensor->setIntegrationTime(VEML6075_INTEGRATION_TIME);
sensor->setDynamicMode(VEML6075_DYNAMIC_MODE);
_sensors.push_back(sensor);
}
#endif
#if VL53L1X_SUPPORT
{
VL53L1XSensor * sensor = new VL53L1XSensor();
sensor->setInterMeasurementPeriod(VL53L1X_INTER_MEASUREMENT_PERIOD);
sensor->setDistanceMode(VL53L1X_DISTANCE_MODE);
sensor->setMeasurementTimingBudget(VL53L1X_MEASUREMENT_TIMING_BUDGET);
_sensors.push_back(sensor);
}
#endif
#if EZOPH_SUPPORT
{
EZOPHSensor * sensor = new EZOPHSensor();
sensor->setRX(EZOPH_RX_PIN);
sensor->setTX(EZOPH_TX_PIN);
_sensors.push_back(sensor);
}
#endif
#if ADE7953_SUPPORT
{
ADE7953Sensor * sensor = new ADE7953Sensor();
sensor->setAddress(ADE7953_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if SI1145_SUPPORT
{
SI1145Sensor * sensor = new SI1145Sensor();
sensor->setAddress(SI1145_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if HDC1080_SUPPORT
{
HDC1080Sensor * sensor = new HDC1080Sensor();
sensor->setAddress(HDC1080_ADDRESS);
_sensors.push_back(sensor);
}
#endif
#if PZEM004TV30_SUPPORT
{
auto rx = getSetting("pzemv30RX", PZEM004TV30Sensor::RxPin);
auto tx = getSetting("pzemv30TX", PZEM004TV30Sensor::TxPin);
//TODO: getSetting("pzemv30*Cfg", (SW)SERIAL_8N1); ?
//TODO: getSetting("serial*Cfg", ...); and attach index of the port ?
//TODO: more than one sensor on port, like the v1
PZEM004TV30Sensor::PortPtr port;
if (getSetting("pzemSoft", PZEM004TV30Sensor::useSoftwareSerial())) {
port = PZEM004TV30Sensor::makeSoftwarePort(rx, tx);
} else {
port = PZEM004TV30Sensor::makeHardwarePort(
PZEM004TV30Sensor::defaultHardwarePort(), rx, tx);
}
if (!port) {
return;
}
auto* sensor = PZEM004TV30Sensor::make(std::move(port),
getSetting("pzemv30Addr", PZEM004TV30Sensor::DefaultAddress),
getSetting("pzemv30ReadTimeout", PZEM004TV30Sensor::DefaultReadTimeout));
sensor->setDebug(getSetting("pzemv30Debug", PZEM004TV30Sensor::DefaultDebug));
_sensors.push_back(sensor);
}
#endif
}
String _magnitudeTopicIndex(const sensor_magnitude_t& magnitude) {
char buffer[32] = {0};
String topic { _magnitudeTopic(magnitude.type) };
if (SENSOR_USE_INDEX || (sensor_magnitude_t::counts(magnitude.type) > 1)) {
snprintf(buffer, sizeof(buffer), "%s/%u", topic.c_str(), magnitude.index_global);
} else {
snprintf(buffer, sizeof(buffer), "%s", topic.c_str());
}
return String(buffer);
}
void _sensorReport(unsigned char index, const sensor_magnitude_t& magnitude) {
// XXX: dtostrf only handles basic floating point values and will never produce scientific notation
// ensure decimals is within some sane limit and the actual value never goes above this buffer size
char buffer[64];
dtostrf(magnitude.reported, 1, magnitude.decimals, buffer);
for (auto& handler : _magnitude_report_handlers) {
handler(_magnitudeTopic(magnitude.type), magnitude.index_global, magnitude.reported, buffer);
}
#if MQTT_SUPPORT
{
const String topic(_magnitudeTopicIndex(magnitude));
mqttSend(topic.c_str(), buffer);
#if SENSOR_PUBLISH_ADDRESSES
String address_topic;
address_topic.reserve(topic.length() + 1 + strlen(SENSOR_ADDRESS_TOPIC));
address_topic += F(SENSOR_ADDRESS_TOPIC);
address_topic += '/';
address_topic += topic;
mqttSend(address_topic.c_str(), magnitude.sensor->address(magnitude.slot).c_str());
#endif // SENSOR_PUBLISH_ADDRESSES
}
#endif // MQTT_SUPPORT
// TODO: both integrations depend on the absolute index instead of specific type
// so, we still need to pass / know the 'global' index inside of _magnitudes[]
#if THINGSPEAK_SUPPORT
tspkEnqueueMeasurement(index, buffer);
#endif // THINGSPEAK_SUPPORT
#if DOMOTICZ_SUPPORT
domoticzSendMagnitude(magnitude.type, index, magnitude.reported, buffer);
#endif // DOMOTICZ_SUPPORT
}
void _sensorInit() {
_sensors_ready = true;
for (auto& sensor : _sensors) {
// Do not process an already initialized sensor
if (sensor->ready()) {
continue;
}
// Force sensor to reload config
DEBUG_MSG_P(PSTR("[SENSOR] Initializing %s\n"), sensor->description().c_str());
sensor->begin();
if (!sensor->ready()) {
if (0 != sensor->error()) {
DEBUG_MSG_P(PSTR("[SENSOR] -> ERROR %d\n"), sensor->error());
}
_sensors_ready = false;
break;
}
// Initialize sensor magnitudes
for (unsigned char magnitude_slot = 0; magnitude_slot < sensor->count(); ++magnitude_slot) {
const auto magnitude_type = sensor->type(magnitude_slot);
_magnitudes.emplace_back(
magnitude_slot, // id of the magnitude, unique to the sensor
magnitude_type, // cache type as well, no need to call type(slot) again
sensor::Unit::None, // set by configuration, default for now
sensor // bind the sensor to allow us to reference it later
);
}
// Custom initializations for analog sensors
// (but, notice that this is global across all sensors of this type!)
if (_sensorIsAnalog(sensor)) {
_sensorAnalogInit(static_cast<BaseAnalogSensor*>(sensor));
}
}
// Energy tracking is implemented by looking at the specific magnitude & it's index at read time
// TODO: shuffle some functions around so that debug can be in the init func instead and still be inline?
for (auto& magnitude : _magnitudes) {
if (_sensorIsEmon(magnitude.sensor) && (MAGNITUDE_ENERGY == magnitude.type)) {
_sensorTrackEnergyTotal(magnitude);
DEBUG_MSG_P(PSTR("[ENERGY] Tracking %s/%u for %s\n"),
_magnitudeTopic(magnitude.type).c_str(),
magnitude.index_global,
magnitude.sensor->description().c_str());
}
}
if (_sensors_ready) {
DEBUG_MSG_P(PSTR("[SENSOR] Finished initialization for %zu sensor(s) and %zu magnitude(s)\n"),
_sensors.size(), _magnitudes.size());
}
}
void _sensorConfigure() {
// Read interval is shared between every sensor
// TODO: implement scheduling in the sensor itself.
// allow reads faster than 1sec, not just internal ones via tick()
// allow 'manual' sensors that may be triggered programatically
_sensor_read_interval = sensor::settings::readInterval();
_sensor_init_interval = sensor::settings::initInterval();
_sensor_report_every = sensor::settings::reportEvery();
// TODO: something more generic? energy is an accumulating value, only allow for similar ones?
// TODO: move to an external module?
_sensor_energy_tracker.every(sensor::settings::saveEvery());
_sensor_real_time = sensor::settings::realTimeValues();
// Update magnitude config, filter sizes and reset energy if needed
{
for (auto& magnitude : _magnitudes) {
// process emon-specific settings first. ensure that settings use global index and we access sensor with the local one
if (_sensorIsEmon(magnitude.sensor) && _magnitudeRatioSupported(magnitude.type)) {
auto* sensor = static_cast<BaseEmonSensor*>(magnitude.sensor);
sensor->setRatio(magnitude.slot, _magnitudeSettingsRatio(magnitude, sensor->defaultRatio(magnitude.slot)));
}
// analog variant of emon sensor has some additional settings
if (_sensorIsAnalogEmon(magnitude.sensor) && (magnitude.type == MAGNITUDE_VOLTAGE)) {
auto* sensor = static_cast<BaseAnalogEmonSensor*>(magnitude.sensor);
sensor->setVoltage(
getSetting({_magnitudeSettingsKey(magnitude, F("Mains")), magnitude.index_global},
sensor->defaultVoltage()));
sensor->setReferenceVoltage(
getSetting({_magnitudeSettingsKey(magnitude, F("Reference")), magnitude.index_global},
sensor->defaultReferenceVoltage()));
}
// adjust units based on magnitude's type
magnitude.units = _magnitudeUnitFilter(magnitude,
getSetting(_magnitudeSettingsUnitsKey(magnitude), magnitude.sensor->units(magnitude.slot)));
// adjust resulting value (simple plus or minus)
// TODO: inject math or rpnlib expression?
{
if (_magnitudeCorrectionSupported(magnitude.type)) {
magnitude.correction = getSetting(
_magnitudeSettingsCorrectionKey(magnitude),
_magnitudeCorrection(magnitude.type));
}
}
// pick decimal precision either from our (sane) defaults of from the sensor itself
// (specifically, when sensor has more or less precision than we expect)
{
signed char decimals = magnitude.sensor->decimals(magnitude.units);
magnitude.decimals = (decimals >= 0)
? static_cast<unsigned char>(decimals)
: _sensorUnitDecimals(magnitude.units);
}
// Per-magnitude min & max delta settings for reporting the value
// - ${prefix}DeltaMin${index} controls whether we report when report counter overflows
// (default is set to 0.0 aka value has changed from the last recorded one)
// - ${prefix}DeltaMax${index} will trigger report as soon as read value is greater than the specified delta
// (default is 0.0 as well, but this needs to be >0 to actually do something)
{
magnitude.min_delta = getSetting(
{_magnitudeSettingsKey(magnitude, F("MinDelta")), magnitude.index_global},
sensor::build::DefaultMinDelta
);
magnitude.max_delta = getSetting(
{_magnitudeSettingsKey(magnitude, F("MaxDelta")), magnitude.index_global},
sensor::build::DefaultMaxDelta
);
}
// Sometimes we want to ensure the value is above certain threshold before reporting
{
magnitude.zero_threshold = getSetting(
{_magnitudeSettingsKey(magnitude, F("ZeroThreshold")), magnitude.index_global},
sensor::Value::Unknown
);
}
// in case we don't save energy periodically, purge existing value in ram & settings
if ((MAGNITUDE_ENERGY == magnitude.type) && (0 == _sensor_energy_tracker.every())) {
_sensorResetEnergyTotal(magnitude.index_global);
}
}
}
}
#if SENSOR_DEBUG
void _sensorDebugSetup() {
_magnitude_read_handlers.push_back([](const String& topic, unsigned char index, double, const char* repr) {
DEBUG_MSG_P(PSTR("[SENSOR] %s/%hhu -> %s (%s)\n"),
topic.c_str(), index, repr, _magnitudeUnits(_magnitudes[index]));
});
}
#endif
} // namespace
// -----------------------------------------------------------------------------
// Public
// -----------------------------------------------------------------------------
void sensorOnMagnitudeRead(MagnitudeReadHandler handler) {
_magnitude_read_handlers.push_front(handler);
}
void sensorOnMagnitudeReport(MagnitudeReadHandler handler) {
_magnitude_report_handlers.push_front(handler);
}
unsigned char sensorCount() {
return _sensors.size();
}
unsigned char magnitudeCount() {
return _magnitudes.size();
}
unsigned char magnitudeType(unsigned char index) {
if (index < _magnitudes.size()) {
return _magnitudes[index].type;
}
return MAGNITUDE_NONE;
}
String magnitudeTopic(unsigned char type) {
return _magnitudeTopic(type);
}
double sensor::Value::get() const {
return real_time ? last : reported;
}
String sensor::Value::toString() const {
char buffer[64] { 0 };
dtostrf(real_time ? last : reported, 1, decimals, buffer);
return buffer;
}
sensor::Value magnitudeValue(unsigned char index) {
sensor::Value result;
if (index < _magnitudes.size()) {
const auto& magnitude = _magnitudes[index];
result.real_time = _sensor_real_time;
result.last = magnitude.last;
result.reported = magnitude.reported;
result.decimals = magnitude.decimals;
}
return result;
}
unsigned char magnitudeIndex(unsigned char index) {
if (index < _magnitudes.size()) {
return _magnitudes[index].index_global;
}
return 0;
}
String magnitudeDescription(unsigned char index) {
if (index < _magnitudes.size()) {
return _magnitudeDescription(_magnitudes[index]);
}
return String();
}
String magnitudeTopicIndex(unsigned char index) {
if (index < _magnitudes.size()) {
return _magnitudeTopicIndex(_magnitudes[index]);
}
return String();
}
// -----------------------------------------------------------------------------
namespace {
void _sensorSettingsMigrate(int version) {
// Some keys from older versions were longer
if (version < 3) {
moveSetting("powerUnits", "pwrUnits");
moveSetting("energyUnits", "eneUnits");
}
// Energy is now indexed (based on magnitude.index_global)
// Also update PZEM004T energy total across multiple devices
if (version < 5) {
moveSetting("eneTotal", "eneTotal0");
moveSettings("pzEneTotal", "eneTotal");
}
// Unit ID is no longer shared, drop when equal to Min_ or None
if (version < 5) {
delSetting("pwrUnits");
delSetting("eneUnits");
delSetting("tmpUnits");
}
// Generic pwr settings now have type-specific prefixes
// (index 0, assuming there's only one emon sensor)
if (version < 7) {
moveSetting(F("pwrVoltage"), _magnitudeSettingsKey(MAGNITUDE_VOLTAGE, F("Mains0")));
moveSetting(F("pwrRatioC"), _magnitudeSettingsRatioKey(MAGNITUDE_CURRENT, 0).value());
moveSetting(F("pwrRatioV"), _magnitudeSettingsRatioKey(MAGNITUDE_VOLTAGE, 0).value());
moveSetting(F("pwrRatioP"), _magnitudeSettingsRatioKey(MAGNITUDE_POWER_ACTIVE, 0).value());
moveSetting(F("pwrRatioE"), _magnitudeSettingsRatioKey(MAGNITUDE_ENERGY, 0).value());
}
#if HLW8012_SUPPORT
if (version < 9) {
moveSetting(F("snsHlw8012SelGPIO"), F("hlw8012SEL"));
moveSetting(F("snsHlw8012CfGPIO"), F("hlw8012CF"));
moveSetting(F("snsHlw8012Cf1GPIO"), F("hlw8012CF1"));
}
#endif
if (version < 11) {
moveSetting(F("apiRealTime"), F("snsRealTime"));
moveSetting(F("tmpMinDelta"), _magnitudeSettingsKey(MAGNITUDE_TEMPERATURE, F("MinDelta0")));
moveSetting(F("humMinDelta"), _magnitudeSettingsKey(MAGNITUDE_HUMIDITY, F("MinDelta0")));
moveSetting(F("eneMaxDelta"), _magnitudeSettingsKey(MAGNITUDE_ENERGY, F("MaxDelta0")));
}
}
} // namespace
void sensorSetup() {
// Settings backwards compatibility
migrateVersion(_sensorSettingsMigrate);
// Load configured sensors and set up all of magnitudes
_sensorLoad();
_sensorInit();
// Configure based on settings
_sensorConfigure();
// Allow us to query key default
settingsRegisterQueryHandler({
.check = _sensorCheckKeyPrefix,
.get = _sensorQueryHandler
});
// Websockets integration, send sensor readings and configuration
#if WEB_SUPPORT
wsRegister()
.onVisible(_sensorWebSocketOnVisible)
.onConnected(_sensorWebSocketOnConnectedInitial)
.onConnected(_sensorWebSocketOnConnectedList)
.onConnected(_sensorWebSocketOnConnectedSettings)
.onData(_sensorWebSocketSendData)
.onAction(_sensorWebSocketOnAction)
.onKeyCheck(_sensorWebSocketOnKeyCheck);
#endif
// MQTT receive callback, atm only for energy reset
#if MQTT_SUPPORT
mqttRegister(_sensorMqttCallback);
#endif
// API
#if API_SUPPORT
_sensorApiSetup();
#endif
// Terminal
#if TERMINAL_SUPPORT
_sensorInitCommands();
#endif
#if SENSOR_DEBUG
_sensorDebugSetup();
#endif
// Main callbacks
espurnaRegisterLoop(sensorLoop);
espurnaRegisterReload(_sensorConfigure);
}
void sensorLoop() {
// Continiously repeat initialization if there are still some un-initialized sensors after setup()
using TimeSource = espurna::time::CoreClock;
static auto last_init = TimeSource::now();
auto timestamp = TimeSource::now();
if (!_sensors_ready && (timestamp - last_init > _sensor_init_interval)) {
last_init = timestamp;
_sensorInit();
}
if (!_magnitudes.size()) {
return;
}
// Tick hook, called every loop()
_sensorTick();
// But, the actual reading needs to happen at the specified interval
static auto last_update = TimeSource::now();
static int report_count { 0 };
if (timestamp - last_update > _sensor_read_interval) {
last_update = timestamp;
report_count = (report_count + 1) % _sensor_report_every;
sensor::ReadValue value {
.raw = 0.0, // as the sensor returns it
.processed = 0.0, // after applying units and decimals
.filtered = 0.0 // after applying filters, units and decimals
};
// Pre-read hook, called every reading
_sensorPre();
// XXX: Filter out certain magnitude types when relay is turned OFF
#if RELAY_SUPPORT && SENSOR_POWER_CHECK_STATUS
const bool relay_off = (relayCount() == 1) && (relayStatus(0) == 0);
#endif
// Get readings
for (unsigned char magnitude_index = 0; magnitude_index < _magnitudes.size(); ++magnitude_index) {
auto& magnitude = _magnitudes[magnitude_index];
if (!magnitude.sensor->status()) continue;
// -------------------------------------------------------------
// Instant value
// -------------------------------------------------------------
value.raw = magnitude.sensor->value(magnitude.slot);
// Completely remove spurious values if relay is OFF
#if RELAY_SUPPORT && SENSOR_POWER_CHECK_STATUS
switch (magnitude.type) {
case MAGNITUDE_POWER_ACTIVE:
case MAGNITUDE_POWER_REACTIVE:
case MAGNITUDE_POWER_APPARENT:
case MAGNITUDE_POWER_FACTOR:
case MAGNITUDE_CURRENT:
case MAGNITUDE_ENERGY_DELTA:
if (relay_off) {
value.raw = 0.0;
}
break;
default:
break;
}
#endif
// In addition to that, we also check that value is above a certain threshold
if ((!std::isnan(magnitude.zero_threshold)) && ((value.raw < magnitude.zero_threshold))) {
value.raw = 0.0;
}
magnitude.last = value.raw;
magnitude.filter->add(value.raw);
// -------------------------------------------------------------
// Procesing (units and decimals)
// -------------------------------------------------------------
value.processed = _magnitudeProcess(magnitude, value.raw);
{
char buffer[64];
dtostrf(value.processed, 1, magnitude.decimals, buffer);
for (auto& handler : _magnitude_read_handlers) {
handler(_magnitudeTopic(magnitude.type), magnitude.index_global, value.processed, buffer);
}
}
// -------------------------------------------------------------------
// Reporting
// -------------------------------------------------------------------
// Initial status or after report counter overflows
bool report { 0 == report_count };
// In case magnitude was configured with ${name}MaxDelta, override report check
// when the value change is greater than the delta
if (!std::isnan(magnitude.reported) && (magnitude.max_delta > sensor::build::DefaultMaxDelta)) {
report = std::abs(value.processed - magnitude.reported) >= magnitude.max_delta;
}
// Special case for energy, save readings to RAM and EEPROM
if (MAGNITUDE_ENERGY == magnitude.type) {
_magnitudeSaveEnergyTotal(magnitude, report);
}
if (report) {
value.filtered = _magnitudeProcess(magnitude, magnitude.filter->result());
// Make sure that report value is calculated using every read value before it
magnitude.filter->reset();
if (magnitude.filter->size() != _sensor_report_every) {
magnitude.filter->resize(_sensor_report_every);
}
// Check ${name}MinDelta if there is a minimum change threshold to report
if (std::isnan(magnitude.reported) || (std::abs(value.filtered - magnitude.reported) >= magnitude.min_delta)) {
magnitude.reported = value.filtered;
_sensorReport(magnitude_index, magnitude);
}
}
}
// Post-read hook, called every reading
_sensorPost();
// And report data to modules that don't specifically track them
#if WEB_SUPPORT
wsPost(_sensorWebSocketSendData);
#endif
#if THINGSPEAK_SUPPORT
if (report_count == 0) tspkFlush();
#endif
}
}
#endif // SENSOR_SUPPORT