diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 5de94699f0..43f2e12f55 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -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 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) { diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index fd6364c70c..eb33f66909 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -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 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" diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index b731e48e31..f3552f4081 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -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])) diff --git a/tests/test3.yaml b/tests/test3.yaml index e3818d87ec..ec768e17e5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -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'