forked from esphome/esphome
-
Notifications
You must be signed in to change notification settings - Fork 0
Add new component - Emporia Vue #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Maelstrom96
wants to merge
39
commits into
dev
Choose a base branch
from
add-emporia-vue2
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 cb8d759
Update CODEOWNERS for emporia_vue
krconv 3c2b21d
Use generic input string instead of input_phase and input_ct
krconv bcce62b
Add debug log for sensor data
krconv ae4ab06
Add some TODO comments
krconv 3423c4a
Merge pull request #1 from krconv/add-emporia-vue
Maelstrom96 4bf2567
Bunch of changes
Maelstrom96 ee8e896
Other changes
Maelstrom96 b478228
Fixing issues
Maelstrom96 18e840e
Fix lint
Maelstrom96 b22cf68
Fix lint
Maelstrom96 78440be
Adding new const temporarily for External_component
Maelstrom96 285c7ff
Commenting missing constants
Maelstrom96 67f5263
Attempting to fix buffer overflow
Maelstrom96 c2739da
Adding startup delay to i2c task
Maelstrom96 d6710bb
Pumping stack size
Maelstrom96 412a592
Change i2c task behavior to also delay on 0x00
Maelstrom96 3b3bd20
Fixing queue retrieval
Maelstrom96 dcebcbd
Adding the item back to the queue
Maelstrom96 9a1e074
Ignoring invalid messages
Maelstrom96 5e28ad4
Merge branch 'esphome:dev' into add-emporia-vue2
Maelstrom96 441ec97
Minor refactoring and calibration fix
Maelstrom96 56fe6ae
Adding voltage sensor and rename input_color
Maelstrom96 1aab6fa
Misc changes
krconv 8292f41
Rename to avoid confusion with polling components
krconv 940ba45
Merge pull request #2 from krconv/add-logging
Maelstrom96 1bb223c
Merge branch 'esphome:dev' into add-emporia-vue2
Maelstrom96 88e225c
Fix validation to only allow esp_idf
Maelstrom96 2d012e2
Fix OTA issue with i2c task
Maelstrom96 31bd324
Improving OTA status handling for i2c task
Maelstrom96 54c2254
Fixing python lint
Maelstrom96 9f87a7c
fix cpp lint
Maelstrom96 8fba957
Switch to protected per contributing guidelines
Maelstrom96 a218e5f
Fixing cpp lint
Maelstrom96 87dfa1d
Adding tests
Maelstrom96 ec6653e
Fix clang-format
Maelstrom96 f257e5a
Only import if ESP32
Maelstrom96 133fc20
Fix lint
Maelstrom96 b131068
Fix clang-format
Maelstrom96 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| #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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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()orupdate()has no guarantee with the execution time. I was able to see this first hand when testing yesterday whenloop()function of the emporia_vue component was not running fast enough when putting the logger option toVERY_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, allowingtotal_daily_energyto still have a very accurate reading.There was a problem hiding this comment.
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)