Implement SC01_O3 UART Ozone sensor.

This commit is contained in:
TtheCreator 2024-11-16 15:12:47 +01:00
parent e81191ebd2
commit c28e761e2a
6 changed files with 214 additions and 0 deletions

View file

@ -348,6 +348,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/sc01_o3/* @Tthecreator
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
esphome/components/sdl/* @clydebarrow esphome/components/sdl/* @clydebarrow

View file

View file

@ -0,0 +1,114 @@
#include "sc01_o3.h"
static const char *const TAG = "SC01O3Sensor";
namespace esphome {
namespace sc01_o3 {
// The position of various data points in the return packet, excluding the first 0xFF byte.
constexpr uint8_t GAS_NAME_POS = 0;
constexpr uint8_t UNIT_POS = 1;
constexpr uint8_t GAS_CONCENTRATION_HIGH_POS = 3;
constexpr uint8_t GAS_CONCENTRATION_LOW_POS = 4;
constexpr uint8_t CHECKSUM_POS = 7;
// Defined constant
constexpr uint8_t O3_GAS_TYPE = 0x17;
constexpr uint8_t UNIT_PPB = 0x04;
constexpr uint8_t START_BYTE_VALUE = 0xFF;
constexpr std::array<uint8_t, 7> SET_QNA_MODE_COMMAND = {
0x01, // Reserved
0x78, // Change order
0x41, // Q & A mode (This is where we have to ask the device for a
// measurement, instead of the measurement happening every second.)
0x00, 0x00, 0x00, 0x00, // Reserved
};
constexpr std::array<uint8_t, 7> ASK_FOR_DATA_COMMAND = {
0x01, // Reserved
0x86, // Order
0x00, 0x00, 0x00, 0x00, 0x00, // Reserved
};
constexpr std::array<uint8_t, 1> DATA_PREPEND_BYTES = {
START_BYTE_VALUE, // Start byte
};
void SC01O3Sensor::setup() {
if (!this->is_initialized_) {
this->set_qna_mode_();
}
}
void SC01O3Sensor::dump_config() { ESP_LOGCONFIG(TAG, "SC01 O3 sensor!"); }
uint8_t SC01O3Sensor::calculate_checksum(const uint8_t *data, uint8_t len) {
// Calculation is to add up all bytes (except first 0xFF) bytes, then negate it, and add one.
uint8_t out = 0;
for (uint8_t i = 0; i < len; ++i) {
out += data[i];
}
out = (~out) + 1;
return out;
}
void SC01O3Sensor::send_command_(const std::array<uint8_t, 7> data) {
this->write_array(DATA_PREPEND_BYTES);
this->write_array(data);
uint8_t checksum = calculate_checksum(data.data(), data.size());
this->write_array(&checksum, 1);
}
void SC01O3Sensor::set_qna_mode_() {
// Called once during setup
send_command_(SET_QNA_MODE_COMMAND);
ESP_LOGI(TAG, "O3 Sensor setup complete.");
this->is_initialized_ = true;
}
void SC01O3Sensor::update() {
if (!this->is_initialized_) {
set_qna_mode_();
}
ESP_LOGD(TAG, "Update....");
send_command_(SET_QNA_MODE_COMMAND);
send_command_(ASK_FOR_DATA_COMMAND);
}
void SC01O3Sensor::loop() {
// ESP_LOGI(TAG, "Loop....");
if (!this->is_initialized_) {
set_qna_mode_();
}
// Polling function to read data from UART
while (this->available() >= 9) {
// Read the first byte and check for the start byte (0xFF)
uint8_t first_byte = this->read();
if (first_byte != START_BYTE_VALUE) {
continue; // Skip and search for the next start byte
}
// If we find START_BYTE_VALUE, attempt to read the rest of the packet
uint8_t buffer[8]; // Excluding the start byte
this->read_array(buffer, 8);
ESP_LOGD(TAG, "Received data: %x %x %x %x %x %x %x %x", buffer[0], buffer[1], buffer[2], buffer[3], buffer[4],
buffer[5], buffer[6], buffer[7]);
// Validate packet
if (buffer[GAS_NAME_POS] == O3_GAS_TYPE && buffer[UNIT_POS] == UNIT_PPB &&
buffer[CHECKSUM_POS] == calculate_checksum(buffer, 7)) { // The gas name and unit should be consistent.
uint16_t concentration =
(static_cast<uint16_t>(buffer[GAS_CONCENTRATION_HIGH_POS]) << 8) | buffer[GAS_CONCENTRATION_LOW_POS];
state = concentration;
this->publish_state(concentration);
ESP_LOGI(TAG, "Valid data: %u ppb", concentration);
} else {
ESP_LOGW(TAG, "Invalid data received after start byte");
}
}
}
} // namespace sc01_o3
} // namespace esphome

View file

@ -0,0 +1,29 @@
#pragma once
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart_component.h"
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
namespace esphome {
namespace sc01_o3 {
class SC01O3Sensor : public sensor::Sensor, public PollingComponent, public uart::UARTDevice {
public:
void setup() override;
void update() override;
void loop() override;
void dump_config() override;
uint16_t state = 0;
protected:
bool is_initialized_ = false;
void set_qna_mode_();
static uint8_t calculate_checksum(const uint8_t *data, uint8_t len);
void send_command_(std::array<uint8_t, 7> data);
};
} // namespace sc01_o3
} // namespace esphome

View file

@ -0,0 +1,56 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_OZONE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_BILLION,
)
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@Tthecreator"]
SC01O3_ns = cg.esphome_ns.namespace("sc01_o3")
SC01O3Component = SC01O3_ns.class_(
"SC01O3Sensor", sensor.Sensor, cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=3,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_OZONE,
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(
{
cv.GenerateID(): cv.declare_id(SC01O3Component),
cv.Optional(
CONF_UPDATE_INTERVAL, "1000ms"
): cv.positive_time_period_milliseconds,
}
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"sc01_o3",
baud_rate=9600,
require_tx=True,
require_rx=True,
data_bits=8,
parity=None,
stop_bits=1,
)
async def to_code(config):
var = await sensor.new_sensor(config)
# var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))

View file

@ -0,0 +1,14 @@
uart:
tx_pin: GPIO021
rx_pin: GPIO016
baud_rate: 9600
stop_bits: 1
id: uart_bus
sensor:
- platform: sc01_o3
id: o3_sensor
uart_id: uart_bus
name: "Ozone Concentration"
icon: "mdi:chemical-weapon"
update_interval: 5s