From 7f895abc24547aefe8f8b979fd57650bebc15fd0 Mon Sep 17 00:00:00 2001 From: Nad <15346053+valordk@users.noreply.github.com> Date: Wed, 4 Dec 2019 12:34:10 +0100 Subject: [PATCH] Add support for Sensirion SPS30 Particulate Matter sensors (#891) * Add support for Sensirion SPS30 Particulate Matter sensors * Remove blocking of the main thread on initialization; Improve wording on the debug messages; Add robustness in re-initialization of reconnected or replaced sensors; * Fix code formatting; Co-authored-by: Nad --- esphome/components/sps30/__init__.py | 0 esphome/components/sps30/sensor.py | 82 ++++++++ esphome/components/sps30/sps30.cpp | 268 +++++++++++++++++++++++++++ esphome/components/sps30/sps30.h | 62 +++++++ esphome/const.py | 11 ++ tests/test1.yaml | 30 +++ 6 files changed, 453 insertions(+) create mode 100644 esphome/components/sps30/__init__.py create mode 100644 esphome/components/sps30/sensor.py create mode 100644 esphome/components/sps30/sps30.cpp create mode 100644 esphome/components/sps30/sps30.h diff --git a/esphome/components/sps30/__init__.py b/esphome/components/sps30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py new file mode 100644 index 0000000000..c45758be4e --- /dev/null +++ b/esphome/components/sps30/sensor.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_PM_1_0, CONF_PM_2_5, CONF_PM_4_0, CONF_PM_10_0, \ + CONF_PMC_0_5, CONF_PMC_1_0, CONF_PMC_2_5, CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, \ + UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, UNIT_MICROMETER, \ + ICON_CHEMICAL_WEAPON, ICON_COUNTER, ICON_RULER + +DEPENDENCIES = ['i2c'] + +sps30_ns = cg.esphome_ns.namespace('sps30') +SPS30Component = sps30_ns.class_('SPS30Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SPS30Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_4_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PM_SIZE): sensor.sensor_schema(UNIT_MICROMETER, + ICON_RULER, 0), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x69)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_PM_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + cg.add(var.set_pm_1_0_sensor(sens)) + + if CONF_PM_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + + if CONF_PM_4_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_4_0]) + cg.add(var.set_pm_4_0_sensor(sens)) + + if CONF_PM_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + + if CONF_PMC_0_5 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_0_5]) + cg.add(var.set_pmc_0_5_sensor(sens)) + + if CONF_PMC_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_1_0]) + cg.add(var.set_pmc_1_0_sensor(sens)) + + if CONF_PMC_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_2_5]) + cg.add(var.set_pmc_2_5_sensor(sens)) + + if CONF_PMC_4_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_4_0]) + cg.add(var.set_pmc_4_0_sensor(sens)) + + if CONF_PMC_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_10_0]) + cg.add(var.set_pmc_10_0_sensor(sens)) + + if CONF_PM_SIZE in config: + sens = yield sensor.new_sensor(config[CONF_PM_SIZE]) + cg.add(var.set_pm_size_sensor(sens)) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp new file mode 100644 index 0000000000..181bf44189 --- /dev/null +++ b/esphome/components/sps30/sps30.cpp @@ -0,0 +1,268 @@ +#include "sps30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sps30 { + +static const char *TAG = "sps30"; + +static const uint16_t SPS30_CMD_GET_ARTICLE_CODE = 0xD025; +static const uint16_t SPS30_CMD_GET_SERIAL_NUMBER = 0xD033; +static const uint16_t SPS30_CMD_GET_FIRMWARE_VERSION = 0xD100; +static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG = 0x0300; +static const uint16_t SPS30_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SPS30_CMD_READ_MEASUREMENT = 0x0300; +static const uint16_t SPS30_CMD_STOP_MEASUREMENTS = 0x0104; +static const uint16_t SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS = 0x8004; +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; + +void SPS30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up sps30..."); + this->write_command_(SPS30_CMD_SOFT_RESET); + /// Deferred Sensor initialization + this->set_timeout(500, [this]() { + /// Firmware version identification + if (!this->write_command_(SPS30_CMD_GET_FIRMWARE_VERSION)) { + this->error_code_ = FIRMWARE_VERSION_REQUEST_FAILED; + this->mark_failed(); + return; + } + + uint16_t raw_firmware_version[4]; + if (!this->read_data_(raw_firmware_version, 4)) { + this->error_code_ = FIRMWARE_VERSION_READ_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, " Firmware version v%0d.%02d", (raw_firmware_version[0] >> 8), + uint16_t(raw_firmware_version[0] & 0xFF)); + /// Serial number identification + if (!this->write_command_(SPS30_CMD_GET_SERIAL_NUMBER)) { + this->error_code_ = SERIAL_NUMBER_REQUEST_FAILED; + this->mark_failed(); + return; + } + + uint16_t raw_serial_number[8]; + if (!this->read_data_(raw_serial_number, 8)) { + this->error_code_ = SERIAL_NUMBER_READ_FAILED; + this->mark_failed(); + return; + } + + for (size_t i = 0; i < 8; ++i) { + this->serial_number_[i * 2] = static_cast(raw_serial_number[i] >> 8); + this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); + } + ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + this->start_continuous_measurement_(); + }); +} + +void SPS30Component::dump_config() { + ESP_LOGCONFIG(TAG, "sps30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case SERIAL_NUMBER_REQUEST_FAILED: + ESP_LOGW(TAG, "Unable to request sensor serial number"); + break; + case SERIAL_NUMBER_READ_FAILED: + ESP_LOGW(TAG, "Unable to read sensor serial number"); + break; + case FIRMWARE_VERSION_REQUEST_FAILED: + ESP_LOGW(TAG, "Unable to request sensor firmware version"); + break; + case FIRMWARE_VERSION_READ_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Serial Number: '%s'", this->serial_number_); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM4", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); +} + +void SPS30Component::update() { + /// Check if warning flag active (sensor reconnected?) + if (this->status_has_warning()) { + ESP_LOGD(TAG, "Trying to reconnect the sensor..."); + if (this->write_command_(SPS30_CMD_SOFT_RESET)) { + ESP_LOGD(TAG, "Sensor has soft-reset successfully. Waiting for reconnection in 500ms..."); + this->set_timeout(500, [this]() { + this->start_continuous_measurement_(); + /// Sensor restarted and reading attempt made next cycle + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + ESP_LOGD(TAG, "Sensor reconnected successfully. Resuming continuous measurement!"); + }); + } else { + ESP_LOGD(TAG, "Sensor soft-reset failed. Is the sensor offline?"); + } + return; + } + /// Check if measurement is ready before reading the value + if (!this->write_command_(SPS30_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } + + uint16_t raw_read_status[1]; + if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + ESP_LOGD(TAG, "Sensor measurement not ready yet."); + this->skipped_data_read_cycles_++; + /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked + /// as failed so that new sensor is eventually forced to be reinitialized for continuous measurement. + if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) { + ESP_LOGD(TAG, "Sensor exceeded max allowed attempts. Sensor communication will be reinitialized."); + this->status_set_warning(); + } + return; + } + + if (!this->write_command_(SPS30_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement status!"); + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[20]; + if (!this->read_data_(raw_data, 20)) { + ESP_LOGW(TAG, "Error reading measurement data!"); + this->status_set_warning(); + return; + } + + union uint32_float_t { + uint32_t uint32; + float value; + }; + + /// Reading and converting Mass concentration + uint32_float_t pm_1_0{.uint32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1])))}; + uint32_float_t pm_2_5{.uint32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3])))}; + uint32_float_t pm_4_0{.uint32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5])))}; + uint32_float_t pm_10_0{.uint32 = (((uint32_t(raw_data[6])) << 16) | (uint32_t(raw_data[7])))}; + + /// Reading and converting Number concentration + uint32_float_t pmc_0_5{.uint32 = (((uint32_t(raw_data[8])) << 16) | (uint32_t(raw_data[9])))}; + uint32_float_t pmc_1_0{.uint32 = (((uint32_t(raw_data[10])) << 16) | (uint32_t(raw_data[11])))}; + uint32_float_t pmc_2_5{.uint32 = (((uint32_t(raw_data[12])) << 16) | (uint32_t(raw_data[13])))}; + uint32_float_t pmc_4_0{.uint32 = (((uint32_t(raw_data[14])) << 16) | (uint32_t(raw_data[15])))}; + uint32_float_t pmc_10_0{.uint32 = (((uint32_t(raw_data[16])) << 16) | (uint32_t(raw_data[17])))}; + + /// Reading and converting Typical size + uint32_float_t pm_size{.uint32 = (((uint32_t(raw_data[18])) << 16) | (uint32_t(raw_data[19])))}; + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0.value); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5.value); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0.value); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0.value); + + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(pmc_0_5.value); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(pmc_1_0.value); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(pmc_2_5.value); + if (this->pmc_4_0_sensor_ != nullptr) + this->pmc_4_0_sensor_->publish_state(pmc_4_0.value); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(pmc_10_0.value); + + if (this->pm_size_sensor_ != nullptr) + this->pm_size_sensor_->publish_state(pm_size.value); + + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + }); +} + +bool SPS30Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SPS30Component::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SPS30Component::start_continuous_measurement_() { + uint8_t data[4]; + data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; + data[1] = 0x03; + data[2] = 0x00; + data[3] = sht_crc_(0x03, 0x00); + if (!this->write_bytes(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS >> 8, data, 4)) { + ESP_LOGE(TAG, "Error initiating measurements"); + return false; + } + return true; +} + +bool SPS30Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h new file mode 100644 index 0000000000..2f977252a5 --- /dev/null +++ b/esphome/components/sps30/sps30.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sps30 { + +/// This class implements support for the Sensirion SPS30 i2c/UART Particulate Matter +/// PM1.0, PM2.5, PM4, PM10 Air Quality sensors. +class SPS30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_4_0_sensor(sensor::Sensor *pmc_4_0) { pmc_4_0_sensor_ = pmc_4_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + char serial_number_[17] = {0}; /// Terminating NULL character + bool start_continuous_measurement_(); + uint8_t skipped_data_read_cycles_ = 0; + + enum ErrorCode { + COMMUNICATION_FAILED, + FIRMWARE_VERSION_REQUEST_FAILED, + FIRMWARE_VERSION_READ_FAILED, + SERIAL_NUMBER_REQUEST_FAILED, + SERIAL_NUMBER_READ_FAILED, + MEASUREMENT_INIT_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_4_0_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_4_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; + sensor::Sensor *pm_size_sensor_{nullptr}; +}; + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index b604120318..e225862e70 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -329,6 +329,13 @@ CONF_PLATFORMIO_OPTIONS = 'platformio_options' CONF_PM_1_0 = 'pm_1_0' CONF_PM_10_0 = 'pm_10_0' CONF_PM_2_5 = 'pm_2_5' +CONF_PM_4_0 = 'pm_4_0' +CONF_PM_SIZE = 'pm_size' +CONF_PMC_0_5 = 'pmc_0_5' +CONF_PMC_1_0 = 'pmc_1_0' +CONF_PMC_10_0 = 'pmc_10_0' +CONF_PMC_2_5 = 'pmc_2_5' +CONF_PMC_4_0 = 'pmc_4_0' CONF_PORT = 'port' CONF_POSITION = 'position' CONF_POSITION_ACTION = 'position_action' @@ -503,6 +510,7 @@ ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' +ICON_COUNTER = 'mdi:counter' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' @@ -519,6 +527,7 @@ ICON_PULSE = 'mdi:pulse' ICON_RADIATOR = 'mdi:radiator' ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' +ICON_RULER = 'mdi:ruler' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SIGN_DIRECTION = 'mdi:sign-direction' @@ -535,6 +544,7 @@ ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' UNIT_CELSIUS = u'°C' +UNIT_COUNTS_PER_CUBIC_METER = u'#/m³' UNIT_DECIBEL = 'dB' UNIT_DECIBEL_MILLIWATT = 'dBm' UNIT_DEGREE_PER_SECOND = u'°/s' @@ -550,6 +560,7 @@ UNIT_LUX = 'lx' UNIT_METER = 'm' UNIT_METER_PER_SECOND_SQUARED = u'm/s²' UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' +UNIT_MICROMETER = 'µm' UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' UNIT_MICROTESLA = u'µT' UNIT_OHM = u'Ω' diff --git a/tests/test1.yaml b/tests/test1.yaml index 601789a829..f4d6e5dcfe 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -593,6 +593,36 @@ sensor: accuracy_decimals: 1 address: 0x58 update_interval: 5s + - platform: sps30 + pm_1_0: + name: "Workshop PM <1µm Weight concentration" + id: "workshop_PM_1_0" + pm_2_5: + name: "Workshop PM <2.5µm Weight concentration" + id: "workshop_PM_2_5" + pm_4_0: + name: "Workshop PM <4µm Weight concentration" + id: "workshop_PM_4_0" + pm_10_0: + name: "Workshop PM <10µm Weight concentration" + id: "workshop_PM_10_0" + pmc_0_5: + name: "Workshop PM <0.5µm Number concentration" + id: "workshop_PMC_0_5" + pmc_1_0: + name: "Workshop PM <1µm Number concentration" + id: "workshop_PMC_1_0" + pmc_2_5: + name: "Workshop PM <2.5µm Number concentration" + id: "workshop_PMC_2_5" + pmc_4_0: + name: "Workshop PM <4µm Number concentration" + id: "workshop_PMC_4_0" + pmc_10_0: + name: "Workshop PM <10µm Number concentration" + id: "workshop_PMC_10_0" + address: 0x69 + update_interval: 10s - platform: shtcx temperature: name: "Living Room Temperature 10"