Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1ef33e7
Add basic pieces for Emporia Vue component
krconv Nov 27, 2021
cb8d759
Update CODEOWNERS for emporia_vue
krconv Nov 28, 2021
3c2b21d
Use generic input string instead of input_phase and input_ct
krconv Nov 28, 2021
bcce62b
Add debug log for sensor data
krconv Nov 29, 2021
ae4ab06
Add some TODO comments
krconv Nov 30, 2021
3423c4a
Merge pull request #1 from krconv/add-emporia-vue
Maelstrom96 Nov 30, 2021
4bf2567
Bunch of changes
Maelstrom96 Dec 1, 2021
ee8e896
Other changes
Maelstrom96 Dec 1, 2021
b478228
Fixing issues
Maelstrom96 Dec 1, 2021
18e840e
Fix lint
Maelstrom96 Dec 1, 2021
b22cf68
Fix lint
Maelstrom96 Dec 1, 2021
78440be
Adding new const temporarily for External_component
Maelstrom96 Dec 1, 2021
285c7ff
Commenting missing constants
Maelstrom96 Dec 1, 2021
67f5263
Attempting to fix buffer overflow
Maelstrom96 Dec 1, 2021
c2739da
Adding startup delay to i2c task
Maelstrom96 Dec 1, 2021
d6710bb
Pumping stack size
Maelstrom96 Dec 1, 2021
412a592
Change i2c task behavior to also delay on 0x00
Maelstrom96 Dec 1, 2021
3b3bd20
Fixing queue retrieval
Maelstrom96 Dec 1, 2021
dcebcbd
Adding the item back to the queue
Maelstrom96 Dec 1, 2021
9a1e074
Ignoring invalid messages
Maelstrom96 Dec 1, 2021
5e28ad4
Merge branch 'esphome:dev' into add-emporia-vue2
Maelstrom96 Dec 2, 2021
441ec97
Minor refactoring and calibration fix
Maelstrom96 Dec 2, 2021
56fe6ae
Adding voltage sensor and rename input_color
Maelstrom96 Dec 2, 2021
1aab6fa
Misc changes
krconv Dec 4, 2021
8292f41
Rename to avoid confusion with polling components
krconv Dec 4, 2021
940ba45
Merge pull request #2 from krconv/add-logging
Maelstrom96 Dec 4, 2021
1bb223c
Merge branch 'esphome:dev' into add-emporia-vue2
Maelstrom96 Dec 4, 2021
88e225c
Fix validation to only allow esp_idf
Maelstrom96 Dec 4, 2021
2d012e2
Fix OTA issue with i2c task
Maelstrom96 Dec 5, 2021
31bd324
Improving OTA status handling for i2c task
Maelstrom96 Dec 5, 2021
54c2254
Fixing python lint
Maelstrom96 Dec 6, 2021
9f87a7c
fix cpp lint
Maelstrom96 Dec 6, 2021
8fba957
Switch to protected per contributing guidelines
Maelstrom96 Dec 6, 2021
a218e5f
Fixing cpp lint
Maelstrom96 Dec 6, 2021
87dfa1d
Adding tests
Maelstrom96 Dec 6, 2021
ec6653e
Fix clang-format
Maelstrom96 Dec 6, 2021
f257e5a
Only import if ESP32
Maelstrom96 Dec 6, 2021
133fc20
Fix lint
Maelstrom96 Dec 6, 2021
b131068
Fix clang-format
Maelstrom96 Dec 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ esphome/components/dfplayer/* @glmnet
esphome/components/dht/* @OttoWinter
esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/emporia_vue/* @Maelstrom96 @flaviut @krconv
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @jesserockz
esphome/components/esp32_ble_server/* @jesserockz
Expand Down
Empty file.
159 changes: 159 additions & 0 deletions esphome/components/emporia_vue/emporia_vue.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#include "emporia_vue.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"

namespace esphome {
namespace emporia_vue {

static const char *const TAG = "emporia_vue";

// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
EmporiaVueComponent *global_emporia_vue_component = nullptr;

void EmporiaVueComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Emporia Vue");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " Sensor Poll Interval: %dms", this->sensor_poll_interval_);

for (auto *phase : this->phases_) {
std::string wire;
switch (phase->get_input_wire()) {
case PhaseInputWire::BLACK:
wire = "BLACK";
break;
case PhaseInputWire::RED:
wire = "RED";
break;
case PhaseInputWire::BLUE:
wire = "BLUE";
break;
}
ESP_LOGCONFIG(TAG, " Phase Config");
ESP_LOGCONFIG(TAG, " Wire: %s", wire.c_str());
ESP_LOGCONFIG(TAG, " Calibration: %f", phase->get_calibration());
LOG_SENSOR(" ", "Voltage", phase->get_voltage_sensor());
}

for (auto *ct_sensor : this->ct_sensors_) {
LOG_SENSOR(" ", "CT", ct_sensor);
ESP_LOGCONFIG(TAG, " Phase Calibration: %f", ct_sensor->get_phase()->get_calibration());
ESP_LOGCONFIG(TAG, " CT Port Index: %d", ct_sensor->get_input_port());
}
}

void EmporiaVueComponent::setup() {
#ifdef USING_OTA_COMPONENT
// OTA callback to prevent the i2c task to crash
if (this->ota_) {
ESP_LOGV(TAG, "Adding OTA state callback");
this->ota_->add_on_state_callback([this](ota::OTAState state, float var, uint8_t error_code) {
eTaskState i2c_request_task_status = eTaskGetState(this->i2c_request_task_);

if (state == ota::OTAState::OTA_STARTED &&
(i2c_request_task_status == eRunning || i2c_request_task_status == eReady ||
i2c_request_task_status == eBlocked)) {
ESP_LOGV(TAG, "OTA Update started - Suspending i2c_request_task_");
vTaskSuspend(this->i2c_request_task_);
}
if (state == ota::OTAState::OTA_ERROR && i2c_request_task_status == eSuspended) {
ESP_LOGV(TAG, "OTA Update failed - Resuming i2c_request_task_");
vTaskResume(this->i2c_request_task_);
}
});
}
#endif

global_emporia_vue_component = this;

this->i2c_data_queue_ = xQueueCreate(1, sizeof(SensorReading));
xTaskCreatePinnedToCore(&EmporiaVueComponent::i2c_request_task, "i2c_request_task", 4096, nullptr, 0,
&this->i2c_request_task_, 0);
}

void EmporiaVueComponent::i2c_request_task(void *pv) {
const TickType_t poll_interval = global_emporia_vue_component->get_sensor_poll_interval() / portTICK_PERIOD_MS;
TickType_t last_poll;
uint8_t last_sequence_num = 0;

while (true) {
last_poll = xTaskGetTickCount();
SensorReading sensor_reading;

i2c::ErrorCode err =
global_emporia_vue_component->read(reinterpret_cast<uint8_t *>(&sensor_reading), sizeof(sensor_reading));

if (err != i2c::ErrorCode::ERROR_OK) {
ESP_LOGE(TAG, "Failed to read from sensor due to I2C error %d", err);
} else if (sensor_reading.end != 0) {
ESP_LOGE(TAG, "Failed to read from sensor due to a malformed reading, should end in null bytes but is %d",
sensor_reading.end);
} else if (!sensor_reading.is_unread) {
ESP_LOGV(TAG, "Ignoring sensor reading that is marked as read");
} else {
if (last_sequence_num && sensor_reading.sequence_num > last_sequence_num + 1) {
ESP_LOGW(TAG, "Detected %d missing reading(s), data may not be accurate!",
sensor_reading.sequence_num - last_sequence_num - 1);
}

xQueueOverwrite(global_emporia_vue_component->i2c_data_queue_, &sensor_reading);
ESP_LOGV(TAG, "Added sensor reading with sequence number %d to queue", sensor_reading.sequence_num);

last_sequence_num = sensor_reading.sequence_num;
}
vTaskDelayUntil(&last_poll, poll_interval);
}
}

void EmporiaVueComponent::loop() {
SensorReading sensor_reading;

if (xQueueReceive(this->i2c_data_queue_, &sensor_reading, 0) == pdTRUE) {
ESP_LOGV(TAG, "Received sensor reading with sequence number %d from queue", sensor_reading.sequence_num);
for (PhaseConfig *phase : this->phases_) {
phase->update_from_reading(sensor_reading);
}

for (CTSensor *ct_sensor : this->ct_sensors_) {
ct_sensor->update_from_reading(sensor_reading);
}
}
}

void PhaseConfig::update_from_reading(const SensorReading &sensor_reading) {
if (this->voltage_sensor_) {
float calibrated_voltage = sensor_reading.voltage[this->input_wire_] * this->calibration_;
this->voltage_sensor_->publish_state(calibrated_voltage);
}
}

int32_t PhaseConfig::extract_power_for_phase(const ReadingPowerEntry &power_entry) {
switch (this->input_wire_) {
case PhaseInputWire::BLACK:
return power_entry.phase_black;
case PhaseInputWire::RED:
return power_entry.phase_red;
case PhaseInputWire::BLUE:
return power_entry.phase_blue;
default:
ESP_LOGE(TAG, "Unsupported phase input wire, this should never happen");
return -1;
}
}

void CTSensor::update_from_reading(const SensorReading &sensor_reading) {
ReadingPowerEntry power_entry = sensor_reading.power[this->input_port_];
int32_t raw_power = this->phase_->extract_power_for_phase(power_entry);
float calibrated_power = this->get_calibrated_power(raw_power);
this->publish_state(calibrated_power);
}

float CTSensor::get_calibrated_power(int32_t raw_power) const {
float calibration = this->phase_->get_calibration();

float correction_factor = (this->input_port_ < 3) ? 5.5 : 22;

return (raw_power * calibration) / correction_factor;
}

} // namespace emporia_vue
} // namespace esphome
142 changes: 142 additions & 0 deletions esphome/components/emporia_vue/emporia_vue.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#pragma once

#ifdef USE_ESP32

#include <vector>

#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"

#ifdef USING_OTA_COMPONENT
Copy link

@krconv krconv Dec 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the FreeRTOS task is more hassle than it's worth, and will make this harder to maintain and debug in the long run. I think using a more traditional loop/polling approach is easier to understand and won't require the FreeRTOS dependency, global variable, nor specialized OTA setup.
The drawback is that the I2C read takes a while, ~23ms from my testing, which will slow down the loop() by that much every ~0.25s. Considering this hardware is specialized, I think that is ok.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why you say that it's more hassle than it's worth. It's already implemented and it's working as intended.

The main problem I was trying to alleviate with the task is missing readings. Using loop() or update() has no guarantee with the execution time. I was able to see this first hand when testing yesterday when loop() function of the emporia_vue component was not running fast enough when putting the logger option to VERY_VERBOSE, missing multiple sensor readings. That means that the other components could slow the main sensor loop enough that we would be missing power readings.

Right now, I'm only writing a single sensor reading to the queue, but my goal is to eventually add a way to handle a slow loop() and average all the sensor readings in the queue before publishing them, allowing total_daily_energy to still have a very accurate reading.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a config where it's missing readings? I didn't realize that was a concern, I'm glad to test it out too (I have my Vue set up with 18 CT clamp and daily energy sensors, which I'm thinking is close to the most stress the loop() will have)

#include "esphome/components/ota/ota_component.h"
#endif

#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>

namespace esphome {
namespace emporia_vue {

struct __attribute__((__packed__)) ReadingPowerEntry {
int32_t phase_black;
int32_t phase_red;
int32_t phase_blue;
};

struct __attribute__((__packed__)) SensorReading {
bool is_unread;
uint8_t checksum;
uint8_t unknown;
uint8_t sequence_num;

ReadingPowerEntry power[19];

uint16_t voltage[3];
uint16_t frequency;
uint16_t degrees[2];

uint16_t current[19];

uint16_t end;
};

class PhaseConfig;
class CTSensor;

class EmporiaVueComponent : public Component, public i2c::I2CDevice {
public:
void dump_config() override;

void set_sensor_poll_interval(uint32_t sensor_poll_interval) { this->sensor_poll_interval_ = sensor_poll_interval; }
uint32_t get_sensor_poll_interval() const { return this->sensor_poll_interval_; }
void set_phases(std::vector<PhaseConfig *> phases) { this->phases_ = std::move(phases); }
void set_ct_sensors(std::vector<CTSensor *> sensors) { this->ct_sensors_ = std::move(sensors); }
#ifdef USING_OTA_COMPONENT
void set_ota(ota::OTAComponent *ota) { this->ota_ = ota; }
#endif

void setup() override;
void loop() override;

protected:
static void i2c_request_task(void *pv);

uint32_t sensor_poll_interval_;
std::vector<PhaseConfig *> phases_;
std::vector<CTSensor *> ct_sensors_;
QueueHandle_t i2c_data_queue_;
#ifdef USING_OTA_COMPONENT
ota::OTAComponent *ota_{nullptr};
#endif
TaskHandle_t i2c_request_task_;
};

enum PhaseInputWire : uint8_t {
BLACK = 0,
RED = 1,
BLUE = 2,
};

class PhaseConfig {
public:
void set_input_wire(PhaseInputWire input_wire) { this->input_wire_ = input_wire; }
PhaseInputWire get_input_wire() const { return this->input_wire_; }
void set_calibration(float calibration) { this->calibration_ = calibration; }
float get_calibration() const { return this->calibration_; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
sensor::Sensor *get_voltage_sensor() const { return this->voltage_sensor_; }

void update_from_reading(const SensorReading &sensor_reading);

int32_t extract_power_for_phase(const ReadingPowerEntry &power_entry);

protected:
PhaseInputWire input_wire_;
float calibration_;
sensor::Sensor *voltage_sensor_{nullptr};
};

enum CTInputPort : uint8_t {
A = 0,
B = 1,
C = 2,
ONE = 3,
TWO = 4,
THREE = 5,
FOUR = 6,
FIVE = 7,
SIX = 8,
SEVEN = 9,
EIGHT = 10,
NINE = 11,
TEN = 12,
ELEVEN = 13,
TWELVE = 14,
THIRTEEN = 15,
FOURTEEN = 16,
FIFTEEN = 17,
SIXTEEN = 18,
};

class CTSensor : public sensor::Sensor {
public:
void set_phase(PhaseConfig *phase) { this->phase_ = phase; };
const PhaseConfig *get_phase() const { return this->phase_; }
void set_input_port(CTInputPort input_port) { this->input_port_ = input_port; };
CTInputPort get_input_port() const { return this->input_port_; }

void update_from_reading(const SensorReading &sensor_reading);
float get_calibrated_power(int32_t raw_power) const;

protected:
PhaseConfig *phase_;
CTInputPort input_port_;
};

} // namespace emporia_vue
} // namespace esphome

#endif // ifdef USE_ESP32
Loading