PMSX003: Add support for specifying the update interval and spinning down (#3053)

Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
Matthew Garrett 2022-05-10 02:35:43 -07:00 committed by GitHub
parent 782186e13d
commit 98c733108e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 3 deletions

View file

@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens
void PMSX003Component::loop() {
const uint32_t now = millis();
// If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we
// need to keep track of what state we're in.
if (this->update_interval_ > PMS_STABILISING_MS) {
if (this->initialised_ == 0) {
this->send_command_(PMS_CMD_AUTO_MANUAL, 0);
this->send_command_(PMS_CMD_ON_STANDBY, 1);
this->initialised_ = 1;
}
switch (this->state_) {
case PMSX003_STATE_IDLE:
// Power on the sensor now so it'll be ready when we hit the update time
if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS))
return;
this->state_ = PMSX003_STATE_STABILISING;
this->send_command_(PMS_CMD_ON_STANDBY, 1);
this->fan_on_time_ = now;
return;
case PMSX003_STATE_STABILISING:
// wait for the sensor to be stable
if (now - this->fan_on_time_ < PMS_STABILISING_MS)
return;
// consume any command responses that are in the serial buffer
while (this->available())
this->read_byte(&this->data_[0]);
// Trigger a new read
this->send_command_(PMS_CMD_TRIG_MANUAL, 0);
this->state_ = PMSX003_STATE_WAITING;
break;
case PMSX003_STATE_WAITING:
// Just go ahead and read stuff
break;
}
} else if (now - this->last_update_ < this->update_interval_) {
// Otherwise just leave the sensor powered up and come back when we hit the update
// time
return;
}
if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index.
this->data_index_ = 0;
@ -65,6 +106,7 @@ void PMSX003Component::loop() {
// finished
this->parse_data_();
this->data_index_ = 0;
this->last_update_ = now;
} else if (!*check) {
// wrong data
this->data_index_ = 0;
@ -131,6 +173,25 @@ optional<bool> PMSX003Component::check_byte_() {
return {};
}
void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) {
this->data_index_ = 0;
this->data_[data_index_++] = 0x42;
this->data_[data_index_++] = 0x4D;
this->data_[data_index_++] = cmd;
this->data_[data_index_++] = (data >> 8) & 0xFF;
this->data_[data_index_++] = (data >> 0) & 0xFF;
int sum = 0;
for (int i = 0; i < data_index_; i++) {
sum += this->data_[i];
}
this->data_[data_index_++] = (sum >> 8) & 0xFF;
this->data_[data_index_++] = (sum >> 0) & 0xFF;
for (int i = 0; i < data_index_; i++) {
this->write_byte(this->data_[i]);
}
this->data_index_ = 0;
}
void PMSX003Component::parse_data_() {
switch (this->type_) {
case PMSX003_TYPE_5003ST: {
@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() {
}
}
// Spin down the sensor again if we aren't going to need it until more time has
// passed than it takes to stabilise
if (this->update_interval_ > PMS_STABILISING_MS) {
this->send_command_(PMS_CMD_ON_STANDBY, 0);
this->state_ = PMSX003_STATE_IDLE;
}
this->status_clear_warning();
}
uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) {

View file

@ -7,6 +7,13 @@
namespace esphome {
namespace pmsx003 {
// known command bytes
#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically
#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement
#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on
enum PMSX003Type {
PMSX003_TYPE_X003 = 0,
PMSX003_TYPE_5003T,
@ -14,6 +21,12 @@ enum PMSX003Type {
PMSX003_TYPE_5003S,
};
enum PMSX003State {
PMSX003_STATE_IDLE = 0,
PMSX003_STATE_STABILISING,
PMSX003_STATE_WAITING,
};
class PMSX003Component : public uart::UARTDevice, public Component {
public:
PMSX003Component() = default;
@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component {
void set_type(PMSX003Type type) { type_ = type; }
void set_update_interval(uint32_t val) { update_interval_ = val; };
void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor);
void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor);
void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor);
@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component {
protected:
optional<bool> check_byte_();
void parse_data_();
void send_command_(uint8_t cmd, uint16_t data);
uint16_t get_16_bit_uint_(uint8_t start_index);
uint8_t data_[64];
uint8_t data_index_{0};
uint8_t initialised_{0};
uint32_t fan_on_time_{0};
uint32_t last_update_{0};
uint32_t last_transmission_{0};
uint32_t update_interval_{0};
PMSX003State state_{PMSX003_STATE_IDLE};
PMSX003Type type_;
// "Standard Particle"

View file

@ -1,6 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, uart
from esphome.const import (
CONF_FORMALDEHYDE,
CONF_HUMIDITY,
@ -17,6 +18,7 @@ from esphome.const import (
CONF_PM_2_5UM,
CONF_PM_5_0UM,
CONF_PM_10_0UM,
CONF_UPDATE_INTERVAL,
CONF_TEMPERATURE,
CONF_TYPE,
DEVICE_CLASS_PM1,
@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST"
TYPE_PMS5003S = "PMS5003S"
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
PMSX003_TYPES = {
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value):
return value
def validate_update_interval(value):
value = cv.positive_time_period_milliseconds(value)
if value == cv.time_period("0s"):
return value
if value < cv.time_period("30s"):
raise cv.Invalid(
"Update interval must be greater than or equal to 30 seconds if set."
)
return value
CONFIG_SCHEMA = (
cv.Schema(
{
@ -157,6 +171,7 @@ CONFIG_SCHEMA = (
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
}
)
.extend(cv.COMPONENT_SCHEMA)
@ -164,6 +179,17 @@ CONFIG_SCHEMA = (
)
def final_validate(config):
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
schema = uart.final_validate_device_schema(
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
)
schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@ -230,3 +256,5 @@ async def to_code(config):
if CONF_FORMALDEHYDE in config:
sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
cg.add(var.set_formaldehyde_sensor(sens))
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))

View file

@ -266,6 +266,10 @@ uart:
stop_bits: 2
# Specifically added for testing debug with no options at all.
debug:
- id: uart8
tx_pin: GPIO4
rx_pin: GPIO5
baud_rate: 9600
modbus:
uart_id: uart1
@ -559,7 +563,7 @@ sensor:
name: 'AQI'
calculation_type: 'AQI'
- platform: pmsx003
uart_id: uart2
uart_id: uart8
type: PMSX003
pm_1_0:
name: 'PM 1.0 Concentration'
@ -585,8 +589,9 @@ sensor:
name: 'Particulate Count >5.0um'
pm_10_0um:
name: 'Particulate Count >10.0um'
update_interval: 30s
- platform: pmsx003
uart_id: uart2
uart_id: uart5
type: PMS5003T
pm_2_5:
name: 'PM 2.5 Concentration'
@ -595,7 +600,7 @@ sensor:
humidity:
name: 'PMS Humidity'
- platform: pmsx003
uart_id: uart2
uart_id: uart6
type: PMS5003ST
pm_1_0:
name: 'PM 1.0 Concentration'