// -----------------------------------------------------------------------------
|
|
// V9261F based power monitor
|
|
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#if SENSOR_SUPPORT && V9261F_SUPPORT
|
|
|
|
#pragma once
|
|
|
|
#include "BaseEmonSensor.h"
|
|
#include "../libs/fs_math.h"
|
|
|
|
class V9261FSensor : public BaseEmonSensor {
|
|
|
|
public:
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Public
|
|
// ---------------------------------------------------------------------
|
|
|
|
static constexpr Magnitude Magnitudes[] {
|
|
MAGNITUDE_CURRENT,
|
|
MAGNITUDE_VOLTAGE,
|
|
MAGNITUDE_POWER_ACTIVE,
|
|
MAGNITUDE_POWER_REACTIVE,
|
|
MAGNITUDE_POWER_APPARENT,
|
|
MAGNITUDE_POWER_FACTOR,
|
|
MAGNITUDE_ENERGY
|
|
};
|
|
|
|
V9261FSensor() :
|
|
BaseEmonSensor(Magnitudes)
|
|
{}
|
|
|
|
void setPort(Stream* port) {
|
|
_serial = port;
|
|
_dirty = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Sensor API
|
|
// ---------------------------------------------------------------------
|
|
|
|
unsigned char id() const override {
|
|
return SENSOR_V9261F_ID;
|
|
}
|
|
|
|
unsigned char count() const override {
|
|
return std::size(Magnitudes);
|
|
}
|
|
|
|
// Initialization method, must be idempotent
|
|
void begin() override {
|
|
if (!_dirty) return;
|
|
_reading = false;
|
|
_ready = true;
|
|
_dirty = false;
|
|
}
|
|
|
|
// Descriptive name of the sensor
|
|
String description() const override {
|
|
return F("V9261F");
|
|
}
|
|
|
|
// Address of the sensor (it could be the GPIO or I2C address)
|
|
String address(unsigned char) const override {
|
|
return String(V9261F_PORT, 10);
|
|
}
|
|
|
|
// Loop-like method, call it in your main loop
|
|
void tick() override {
|
|
_read();
|
|
}
|
|
|
|
// Type for slot # index
|
|
unsigned char type(unsigned char index) const override {
|
|
if (index < std::size(Magnitudes)) {
|
|
return Magnitudes[index].type;
|
|
}
|
|
|
|
return MAGNITUDE_NONE;
|
|
}
|
|
|
|
// Current value for slot # index
|
|
double value(unsigned char index) override {
|
|
if (index == 0) return _current;
|
|
if (index == 1) return _voltage;
|
|
if (index == 2) return _active;
|
|
if (index == 3) return _reactive;
|
|
if (index == 4) return _apparent;
|
|
if (index == 5) return _apparent > 0 ? 100 * _active / _apparent : 100;
|
|
if (index == 6) return _energy[0].asDouble();
|
|
return 0;
|
|
}
|
|
|
|
double defaultRatio(unsigned char index) const override {
|
|
switch (index) {
|
|
case 0:
|
|
return V9261F_CURRENT_FACTOR;
|
|
case 1:
|
|
return V9261F_VOLTAGE_FACTOR;
|
|
case 2:
|
|
return V9261F_POWER_FACTOR;
|
|
case 3:
|
|
return V9261F_RPOWER_FACTOR;
|
|
}
|
|
|
|
return BaseEmonSensor::DefaultRatio;
|
|
}
|
|
|
|
void setRatio(unsigned char index, double value) override {
|
|
if (value > 0.0) {
|
|
switch (index) {
|
|
case 0:
|
|
_current_ratio = value;
|
|
break;
|
|
case 1:
|
|
_voltage_ratio = value;
|
|
break;
|
|
case 2:
|
|
_power_active_ratio = value;
|
|
break;
|
|
case 3:
|
|
_power_reactive_ratio = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
double getRatio(unsigned char index) const override {
|
|
switch (index) {
|
|
case 0:
|
|
return _current_ratio;
|
|
case 1:
|
|
return _voltage_ratio;
|
|
case 2:
|
|
return _power_active_ratio;
|
|
case 3:
|
|
return _power_reactive_ratio;
|
|
}
|
|
|
|
return BaseEmonSensor::getRatio(index);
|
|
}
|
|
|
|
protected:
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Protected
|
|
// ---------------------------------------------------------------------
|
|
|
|
void _read() {
|
|
|
|
// we are seeing the data request
|
|
if (_state == 0) {
|
|
const auto available = _serial->available();
|
|
if (available <= 0) {
|
|
if (_found && (TimeSource::now() - _timestamp > SyncInterval)) {
|
|
_index = 0;
|
|
_state = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
consumeAvailable(*_serial);
|
|
_found = true;
|
|
_timestamp = TimeSource::now();
|
|
|
|
// ...which we just skip...
|
|
} else if (_state == 1) {
|
|
|
|
_index += consumeAvailable(*_serial);
|
|
if (_index++ >= 7) {
|
|
_index = 0;
|
|
_state = 2;
|
|
}
|
|
|
|
// ...until we receive response...
|
|
} else if (_state == 2) {
|
|
|
|
const auto available = _serial->available();
|
|
if (available <= 0) {
|
|
return;
|
|
}
|
|
|
|
_index += _serial->read(&_data[_index], std::min(
|
|
static_cast<size_t>(available), sizeof(_data)));
|
|
if (_index >= 19) {
|
|
_timestamp = TimeSource::now();
|
|
_state = 3;
|
|
}
|
|
|
|
// validate received data and wait for the next request -> response
|
|
// FE1104 25F2420069C1BCFF20670C38C05E4101 B6
|
|
// ^^^^^^ - HEAD byte, mask, number of valeus
|
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - u32 4 times
|
|
// ^^ - CRC byte
|
|
} else if (_state == 3) {
|
|
|
|
if (_checksum(&_data[0], &_data[19]) == _data[19]) {
|
|
|
|
_active = (double) (
|
|
(_data[3]) +
|
|
(_data[4] << 8) +
|
|
(_data[5] << 16) +
|
|
(_data[6] << 24)
|
|
) / _power_active_ratio;
|
|
|
|
_reactive = (double) (
|
|
(_data[7]) +
|
|
(_data[8] << 8) +
|
|
(_data[9] << 16) +
|
|
(_data[10] << 24)
|
|
) / _power_reactive_ratio;
|
|
|
|
_voltage = (double) (
|
|
(_data[11]) +
|
|
(_data[12] << 8) +
|
|
(_data[13] << 16) +
|
|
(_data[14] << 24)
|
|
) / _voltage_ratio;
|
|
|
|
_current = (double) (
|
|
(_data[15]) +
|
|
(_data[16] << 8) +
|
|
(_data[17] << 16) +
|
|
(_data[18] << 24)
|
|
) / _current_ratio;
|
|
|
|
if (_active < 0) _active = 0;
|
|
if (_reactive < 0) _reactive = 0;
|
|
if (_voltage < 0) _voltage = 0;
|
|
if (_current < 0) _current = 0;
|
|
|
|
_apparent = fs_sqrt(_reactive * _reactive + _active * _active);
|
|
|
|
const auto now = TimeSource::now();
|
|
if (_reading) {
|
|
using namespace espurna::sensor;
|
|
const auto elapsed = std::chrono::duration_cast<espurna::duration::Seconds>(now - _last_reading);
|
|
_energy[0] += WattSeconds(Watts{_active}, elapsed);
|
|
}
|
|
|
|
_reading = true;
|
|
_last_reading = now;
|
|
|
|
}
|
|
|
|
_timestamp = TimeSource::now();
|
|
_index = 0;
|
|
_state = 4;
|
|
|
|
// ... by waiting for a bit
|
|
} else if (_state == 4) {
|
|
|
|
consumeAvailable(*_serial);
|
|
if (TimeSource::now() - _timestamp > SyncInterval) {
|
|
_state = 1;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
static uint8_t _checksum(const uint8_t* begin, const uint8_t* end) {
|
|
uint8_t out = 0;
|
|
for (auto it = begin; it != end; ++it) {
|
|
out += (*it);
|
|
}
|
|
out = ~out + 0x33;
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
|
|
Stream* _serial { nullptr };
|
|
|
|
using TimeSource = espurna::time::CoreClock;
|
|
static constexpr auto SyncInterval = TimeSource::duration { V9261F_SYNC_INTERVAL };
|
|
|
|
double _active { 0 };
|
|
double _reactive { 0 };
|
|
double _voltage { 0 };
|
|
double _current { 0 };
|
|
double _apparent { 0 };
|
|
|
|
TimeSource::time_point _last_reading;
|
|
TimeSource::time_point _timestamp;
|
|
|
|
int _state { 0 };
|
|
bool _found { false };
|
|
bool _reading { false };
|
|
|
|
uint8_t _data[24] {0};
|
|
size_t _index { 0 };
|
|
|
|
};
|
|
|
|
#if __cplusplus < 201703L
|
|
constexpr BaseSensor::Magnitude V9261FSensor::Magnitudes[];
|
|
#endif
|
|
|
|
#endif // SENSOR_SUPPORT && V9261F_SUPPORT
|