[sps30] Add idle mode functionality (#12255)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
c0mputerguru
2025-12-05 10:33:00 -08:00
committed by GitHub
parent 7f7c913a85
commit 78bef42473
5 changed files with 92 additions and 9 deletions

View File

@@ -1,20 +1,25 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "sps30.h"
namespace esphome {
namespace sps30 {
template<typename... Ts> class StartFanAction : public Action<Ts...> {
template<typename... Ts> class StartFanAction : public Action<Ts...>, public Parented<SPS30Component> {
public:
explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {}
void play(const Ts &...x) override { this->parent_->start_fan_cleaning(); }
};
void play(const Ts &...x) override { this->sps30_->start_fan_cleaning(); }
template<typename... Ts> class StartMeasurementAction : public Action<Ts...>, public Parented<SPS30Component> {
public:
void play(const Ts &...x) override { this->parent_->start_measurement(); }
};
protected:
SPS30Component *sps30_;
template<typename... Ts> class StopMeasurementAction : public Action<Ts...>, public Parented<SPS30Component> {
public:
void play(const Ts &...x) override { this->parent_->stop_measurement(); }
};
} // namespace sps30

View File

@@ -38,8 +38,11 @@ SPS30Component = sps30_ns.class_(
# Actions
StartFanAction = sps30_ns.class_("StartFanAction", automation.Action)
StartMeasurementAction = sps30_ns.class_("StartMeasurementAction", automation.Action)
StopMeasurementAction = sps30_ns.class_("StopMeasurementAction", automation.Action)
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
CONF_IDLE_INTERVAL = "idle_interval"
CONFIG_SCHEMA = (
cv.Schema(
@@ -109,6 +112,7 @@ CONFIG_SCHEMA = (
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval,
cv.Optional(CONF_IDLE_INTERVAL): cv.update_interval,
}
)
.extend(cv.polling_component_schema("60s"))
@@ -164,6 +168,9 @@ async def to_code(config):
if CONF_AUTO_CLEANING_INTERVAL in config:
cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL]))
if CONF_IDLE_INTERVAL in config:
cg.add(var.set_idle_interval(config[CONF_IDLE_INTERVAL]))
SPS30_ACTION_SCHEMA = maybe_simple_id(
{
@@ -175,6 +182,13 @@ SPS30_ACTION_SCHEMA = maybe_simple_id(
@automation.register_action(
"sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA
)
async def sps30_fan_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"sps30.start_measurement", StartMeasurementAction, SPS30_ACTION_SCHEMA
)
@automation.register_action(
"sps30.stop_measurement", StopMeasurementAction, SPS30_ACTION_SCHEMA
)
async def sps30_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -20,6 +20,7 @@ static const uint16_t SPS30_CMD_START_FAN_CLEANING = 0x5607;
static const uint16_t SPS30_CMD_SOFT_RESET = 0xD304;
static const size_t SERIAL_NUMBER_LENGTH = 8;
static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5;
static const uint32_t SPS30_WARM_UP_SEC = 30;
void SPS30Component::setup() {
this->write_command(SPS30_CMD_SOFT_RESET);
@@ -63,6 +64,8 @@ void SPS30Component::setup() {
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
this->start_continuous_measurement_();
this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000;
this->next_state_ = READ;
this->setup_complete_ = true;
});
});
@@ -101,6 +104,9 @@ void SPS30Component::dump_config() {
" Serial number: %s\n"
" Firmware version v%0d.%0d",
this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF);
if (this->idle_interval_.has_value()) {
ESP_LOGCONFIG(TAG, " Idle interval: %us", this->idle_interval_.value() / 1000);
}
LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_);
LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_);
@@ -132,6 +138,26 @@ void SPS30Component::update() {
}
return;
}
// If its not time to take an action, do nothing.
const uint32_t update_start_ms = millis();
if (this->next_state_ != NONE && (int32_t) (this->next_state_ms_ - update_start_ms) > 0) {
ESP_LOGD(TAG, "Sensor waiting for %ums before transitioning to state %d.", (this->next_state_ms_ - update_start_ms),
this->next_state_);
return;
}
switch (this->next_state_) {
case WAKE:
this->start_measurement();
return;
case NONE:
return;
case READ:
// Read logic continues below
break;
}
/// Check if measurement is ready before reading the value
if (!this->write_command(SPS30_CMD_GET_DATA_READY_STATUS)) {
this->status_set_warning();
@@ -211,6 +237,16 @@ void SPS30Component::update() {
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
// Stop measurements and wait if we have an idle interval. If not using idle mode, let the next state just execute
// on next update.
if (this->idle_interval_.has_value()) {
this->stop_measurement();
this->next_state_ms_ = millis() + this->idle_interval_.value();
this->next_state_ = WAKE;
} else {
this->next_state_ms_ = millis();
}
});
}
@@ -219,6 +255,26 @@ bool SPS30Component::start_continuous_measurement_() {
ESP_LOGE(TAG, "Error initiating measurements");
return false;
}
ESP_LOGD(TAG, "Started measurements");
// Notify the state machine to wait the warm up interval before reading
this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000;
this->next_state_ = READ;
return true;
}
bool SPS30Component::start_measurement() { return start_continuous_measurement_(); }
bool SPS30Component::stop_measurement() {
if (!write_command(SPS30_CMD_STOP_MEASUREMENTS)) {
ESP_LOGE(TAG, "Error stopping measurements");
return false;
} else {
ESP_LOGD(TAG, "Stopped measurements");
// Exit the state machine if measurement is stopped.
this->next_state_ms_ = 0;
this->next_state_ = NONE;
}
return true;
}

View File

@@ -23,17 +23,23 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; }
void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; }
void set_idle_interval(uint32_t idle_interval) { idle_interval_ = idle_interval; }
void setup() override;
void update() override;
void dump_config() override;
bool start_fan_cleaning();
bool stop_measurement();
bool start_measurement();
protected:
bool setup_complete_{false};
uint16_t raw_firmware_version_;
char serial_number_[17] = {0}; /// Terminating NULL character
uint8_t skipped_data_read_cycles_ = 0;
uint32_t next_state_ms_ = 0;
enum NextState : uint8_t { WAKE, READ, NONE } next_state_{NONE};
bool start_continuous_measurement_();
@@ -58,6 +64,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
sensor::Sensor *pmc_10_0_sensor_{nullptr};
sensor::Sensor *pm_size_sensor_{nullptr};
optional<uint32_t> fan_interval_;
optional<uint32_t> idle_interval_;
};
} // namespace sps30

View File

@@ -30,3 +30,4 @@ sensor:
id: workshop_PMC_10_0
address: 0x69
update_interval: 10s
idle_interval: 5min