mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:25:35 +00:00
[ufm01] Add UFM-01 ultrasonic flow meter component (#16582)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7cb6cf2f2a
commit
c9095841ae
@@ -561,6 +561,7 @@ esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ufm01/* @ljungqvist
|
||||
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
||||
esphome/components/update/* @jesserockz
|
||||
esphome/components/uponor_smatrix/* @kroimon
|
||||
|
||||
40
esphome/components/ufm01/__init__.py
Normal file
40
esphome/components/ufm01/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@ljungqvist"]
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
ufm01_ns = cg.esphome_ns.namespace("ufm01")
|
||||
UFM01Component = ufm01_ns.class_("UFM01Component", uart.UARTDevice, cg.Component)
|
||||
|
||||
CONF_UFM01_ID = "ufm01_id"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(UFM01Component),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"ufm01",
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
baud_rate=2400,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
52
esphome/components/ufm01/binary_sensor.py
Normal file
52
esphome/components/ufm01/binary_sensor.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import DEVICE_CLASS_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC
|
||||
|
||||
from . import CONF_UFM01_ID, UFM01Component
|
||||
|
||||
DEPENDENCIES = ["ufm01"]
|
||||
|
||||
CONF_UFC_CHIP_ERROR = "ufc_chip_error"
|
||||
CONF_FLOW_DIRECTION_WRONG = "flow_direction_wrong"
|
||||
CONF_EMPTY_TUBE = "empty_tube"
|
||||
CONF_FLOW_RATE_OUT_OF_RANGE = "flow_rate_out_of_range"
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_UFM01_ID): cv.use_id(UFM01Component),
|
||||
cv.Optional(CONF_UFC_CHIP_ERROR): binary_sensor.binary_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, device_class=DEVICE_CLASS_PROBLEM
|
||||
),
|
||||
cv.Optional(CONF_FLOW_DIRECTION_WRONG): binary_sensor.binary_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
),
|
||||
cv.Optional(CONF_EMPTY_TUBE): binary_sensor.binary_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
),
|
||||
cv.Optional(CONF_FLOW_RATE_OUT_OF_RANGE): binary_sensor.binary_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
ufm01_component = await cg.get_variable(config[CONF_UFM01_ID])
|
||||
|
||||
if ufc_chip_error_config := config.get(CONF_UFC_CHIP_ERROR):
|
||||
sens = await binary_sensor.new_binary_sensor(ufc_chip_error_config)
|
||||
cg.add(ufm01_component.set_ufc_chip_error_binary_sensor(sens))
|
||||
|
||||
if flow_direction_wrong_config := config.get(CONF_FLOW_DIRECTION_WRONG):
|
||||
sens = await binary_sensor.new_binary_sensor(flow_direction_wrong_config)
|
||||
cg.add(ufm01_component.set_flow_direction_wrong_binary_sensor(sens))
|
||||
|
||||
if empty_tube_config := config.get(CONF_EMPTY_TUBE):
|
||||
sens = await binary_sensor.new_binary_sensor(empty_tube_config)
|
||||
cg.add(ufm01_component.set_empty_tube_binary_sensor(sens))
|
||||
|
||||
if flow_rate_out_of_range_config := config.get(CONF_FLOW_RATE_OUT_OF_RANGE):
|
||||
sens = await binary_sensor.new_binary_sensor(flow_rate_out_of_range_config)
|
||||
cg.add(ufm01_component.set_flow_rate_out_of_range_binary_sensor(sens))
|
||||
63
esphome/components/ufm01/sensor.py
Normal file
63
esphome/components/ufm01/sensor.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FLOW,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||
DEVICE_CLASS_WATER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_CUBIC_METER_PER_HOUR,
|
||||
UNIT_LITRE,
|
||||
)
|
||||
|
||||
from . import CONF_UFM01_ID, UFM01Component
|
||||
|
||||
DEPENDENCIES = ["ufm01"]
|
||||
|
||||
CONF_ACCUMULATED_FLOW = "accumulated_flow"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_UFM01_ID): cv.use_id(UFM01Component),
|
||||
cv.Optional(CONF_ACCUMULATED_FLOW): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_LITRE,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_WATER,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_FLOW): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
|
||||
accuracy_decimals=5,
|
||||
device_class=DEVICE_CLASS_VOLUME_FLOW_RATE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon="mdi:waves-arrow-right",
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon="mdi:thermometer-water",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
ufm01_component = await cg.get_variable(config[CONF_UFM01_ID])
|
||||
|
||||
if CONF_ACCUMULATED_FLOW in config:
|
||||
sens = await sensor.new_sensor(config[CONF_ACCUMULATED_FLOW])
|
||||
cg.add(ufm01_component.set_accumulated_flow_sensor(sens))
|
||||
|
||||
if CONF_FLOW in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FLOW])
|
||||
cg.add(ufm01_component.set_flow_sensor(sens))
|
||||
|
||||
if CONF_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
|
||||
cg.add(ufm01_component.set_temperature_sensor(sens))
|
||||
234
esphome/components/ufm01/ufm01.cpp
Normal file
234
esphome/components/ufm01/ufm01.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
#include "ufm01.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::ufm01 {
|
||||
|
||||
static const char *const TAG = "ufm01";
|
||||
|
||||
static constexpr uint8_t COMMAND_ACK = 0xE5;
|
||||
static constexpr uint32_t COMMAND_ACK_TIMEOUT_MS = 200;
|
||||
|
||||
static constexpr float L_PER_M3 = 1000.0f;
|
||||
static constexpr float M3_PER_L = 1.0f / L_PER_M3;
|
||||
|
||||
static constexpr std::array<uint8_t, 7> ACTIVE_MODE = {0xFE, 0xFE, 0x11, 0x5C, 0x00, 0x5C, 0x16};
|
||||
static constexpr std::array<uint8_t, 7> CLEAR_ACCUMULATED_FLOW = {0xFE, 0xFE, 0x11, 0x5A, 0xFD, 0x57, 0x16};
|
||||
static constexpr std::array<uint8_t, 7> RESET_DEVICE = {0xFE, 0xFE, 0x11, 0x5D, 0xCB, 0x28, 0x16};
|
||||
|
||||
// Active-mode frame layout (datasheet Table 7)
|
||||
static constexpr size_t FRAME_CHECKSUM_INDEX = 30;
|
||||
static constexpr size_t FRAME_STOP_INDEX = 31;
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
static constexpr uint8_t FRAME_INDEX_INSTANT_FLOW_FLAG = 15;
|
||||
static constexpr uint8_t FRAME_INDEX_RESERVED_SECTION = 21;
|
||||
static constexpr uint8_t FRAME_INDEX_TEMP_FLAG = 24;
|
||||
static constexpr uint8_t FRAME_FLAG_INSTANT_FLOW = 0x0B;
|
||||
static constexpr uint8_t FRAME_FLAG_RESERVED_SECTION = 0x0C;
|
||||
static constexpr uint8_t FRAME_FLAG_TEMP = 0x0D;
|
||||
|
||||
// Measurement decoding
|
||||
static constexpr uint8_t FRAME_ACC_FLOW_FLAG_INDEX = 8;
|
||||
static constexpr uint8_t ACC_FLOW_M3_FLAG = 0x1A;
|
||||
static constexpr uint8_t FRAME_FLOW_SIGN_INDEX = 20;
|
||||
static constexpr uint8_t FLOW_NEGATIVE_SIGN = 0x80;
|
||||
|
||||
// Status bytes (datasheet ST1 / ST2)
|
||||
static constexpr uint8_t FRAME_ST1_INDEX = 28;
|
||||
static constexpr uint8_t FRAME_ST2_INDEX = 29;
|
||||
static constexpr uint8_t ST1_EMPTY_TUBE_MASK = 0x20;
|
||||
static constexpr uint8_t ST2_UFC_ERROR_MASK = 0x20;
|
||||
static constexpr uint8_t ST2_FLOW_DIRECTION_WRONG_MASK = 0x08;
|
||||
static constexpr uint8_t ST2_FLOW_RATE_OUT_OF_RANGE_MASK = 0x04;
|
||||
|
||||
static float to_float(uint8_t data) { return (data >> 4) * 10 + (data & 0x0F); }
|
||||
|
||||
static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t expected, const char *name) {
|
||||
if (data[index] == expected)
|
||||
return true;
|
||||
ESP_LOGW(TAG, "%s (byte %zu) - expected 0x%02X, but was 0x%02X", name, index, expected, data[index]);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool validate_data(uint8_t data[FRAME_SIZE]) {
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < FRAME_CHECKSUM_INDEX; ++i)
|
||||
sum += data[i];
|
||||
return check_byte(data, 0, FRAME_START_BYTE_1, "start byte 1") &&
|
||||
check_byte(data, 1, FRAME_START_BYTE_2, "start byte 2") &&
|
||||
check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
|
||||
check_byte(data, FRAME_INDEX_RESERVED_SECTION, FRAME_FLAG_RESERVED_SECTION, "reserved section flag") &&
|
||||
check_byte(data, FRAME_INDEX_TEMP_FLAG, FRAME_FLAG_TEMP, "temperature flag") &&
|
||||
check_byte(data, FRAME_CHECKSUM_INDEX, sum, "checksum") &&
|
||||
check_byte(data, FRAME_STOP_INDEX, FRAME_STOP_BYTE, "stop byte");
|
||||
}
|
||||
|
||||
static float read_accumulated_flow(uint8_t data[FRAME_SIZE]) {
|
||||
return (data[FRAME_ACC_FLOW_FLAG_INDEX] == ACC_FLOW_M3_FLAG ? L_PER_M3 : 1.0f) *
|
||||
(to_float(data[14]) * 10000000.0f + to_float(data[13]) * 100000.0f + to_float(data[12]) * 1000.0f +
|
||||
to_float(data[11]) * 10.0f + to_float(data[10]) * 0.1f + to_float(data[9]) * 0.001f);
|
||||
}
|
||||
|
||||
static float read_flow(uint8_t data[FRAME_SIZE]) {
|
||||
return (data[FRAME_FLOW_SIGN_INDEX] == FLOW_NEGATIVE_SIGN ? -1.0f : 1.0f) *
|
||||
(to_float(data[19]) * 10000.0f + to_float(data[18]) * 100.0f + to_float(data[17]) +
|
||||
to_float(data[16]) * 0.01f) *
|
||||
M3_PER_L;
|
||||
}
|
||||
|
||||
static void log_hex(const uint8_t *data, size_t len) {
|
||||
char hex_buf[format_hex_pretty_size(FRAME_SIZE)];
|
||||
ESP_LOGD(TAG, "%s", format_hex_pretty_to(hex_buf, data, len, ' '));
|
||||
}
|
||||
|
||||
static float read_temperature(uint8_t data[FRAME_SIZE]) {
|
||||
// happens sometimes before getting a real reading
|
||||
if (data[27] == 0x00 && (data[26] == 0x00 || data[26] == 0x70) && data[25] == 0x00) {
|
||||
return NAN;
|
||||
}
|
||||
return to_float(data[27]) * 100.0f + to_float(data[26]) + to_float(data[25]) * 0.01f;
|
||||
}
|
||||
|
||||
static bool read_ufc_chip_error(const uint8_t data[FRAME_SIZE]) { return data[FRAME_ST2_INDEX] & ST2_UFC_ERROR_MASK; }
|
||||
|
||||
static bool read_flow_direction_wrong(const uint8_t data[FRAME_SIZE]) {
|
||||
return data[FRAME_ST2_INDEX] & ST2_FLOW_DIRECTION_WRONG_MASK;
|
||||
}
|
||||
|
||||
static bool read_empty_tube(const uint8_t data[FRAME_SIZE]) { return data[FRAME_ST1_INDEX] & ST1_EMPTY_TUBE_MASK; }
|
||||
|
||||
static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
|
||||
return data[FRAME_ST2_INDEX] & ST2_FLOW_RATE_OUT_OF_RANGE_MASK;
|
||||
}
|
||||
|
||||
bool UFM01Component::send_command_(const std::array<uint8_t, 7> &command) {
|
||||
this->write_array(command);
|
||||
this->flush();
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < COMMAND_ACK_TIMEOUT_MS) {
|
||||
if (this->available()) {
|
||||
uint8_t byte;
|
||||
if (this->read_byte(&byte)) {
|
||||
if (byte == COMMAND_ACK)
|
||||
return true;
|
||||
ESP_LOGV(TAG, "Unexpected byte while waiting for command ACK: 0x%02X", byte);
|
||||
}
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UFM01Component::reset_device_() { return this->send_command_(RESET_DEVICE); }
|
||||
|
||||
bool UFM01Component::clear_accumulated_flow_() { return this->send_command_(CLEAR_ACCUMULATED_FLOW); }
|
||||
|
||||
bool UFM01Component::set_active_mode_() { return this->send_command_(ACTIVE_MODE); }
|
||||
|
||||
float UFM01Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
void UFM01Component::setup() {
|
||||
ESP_LOGI(TAG, "Setting up UFM-01...");
|
||||
if (!this->set_active_mode_()) {
|
||||
ESP_LOGW(TAG, "Failed to set active mode (no ACK from device)");
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void UFM01Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "UFM-01:");
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Accumulated Flow", this->accumulated_flow_sensor_);
|
||||
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "UFC Chip Error", this->ufc_chip_error_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Flow Direction Wrong", this->flow_direction_wrong_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
|
||||
#endif
|
||||
this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGW(TAG, "Setup failed: active mode not acknowledged by device");
|
||||
}
|
||||
}
|
||||
|
||||
void UFM01Component::on_data_(uint8_t data[FRAME_SIZE]) {
|
||||
bool empty_tube = read_empty_tube(data);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->ufc_chip_error_binary_sensor_ != nullptr)
|
||||
this->ufc_chip_error_binary_sensor_->publish_state(read_ufc_chip_error(data));
|
||||
if (this->flow_direction_wrong_binary_sensor_ != nullptr)
|
||||
this->flow_direction_wrong_binary_sensor_->publish_state(read_flow_direction_wrong(data));
|
||||
if (this->empty_tube_binary_sensor_ != nullptr)
|
||||
this->empty_tube_binary_sensor_->publish_state(empty_tube);
|
||||
if (this->flow_rate_out_of_range_binary_sensor_ != nullptr)
|
||||
this->flow_rate_out_of_range_binary_sensor_->publish_state(read_flow_rate_out_of_range(data));
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Total volume remains valid when the tube is dry; flow and temperature are not.
|
||||
if (this->accumulated_flow_sensor_ != nullptr)
|
||||
this->accumulated_flow_sensor_->publish_state(read_accumulated_flow(data));
|
||||
|
||||
if (empty_tube) {
|
||||
if (this->flow_sensor_ != nullptr)
|
||||
this->flow_sensor_->publish_state(NAN);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(NAN);
|
||||
} else {
|
||||
if (this->flow_sensor_ != nullptr)
|
||||
this->flow_sensor_->publish_state(read_flow(data));
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(read_temperature(data));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UFM01Component::loop() {
|
||||
// Drain the UART buffer each loop, reading one byte at a time into the frame
|
||||
while (this->available()) {
|
||||
if (!this->read_byte(&this->data_[this->read_index_])) {
|
||||
ESP_LOGW(TAG, "unable to read byte");
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
|
||||
(this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
|
||||
ESP_LOGW(TAG, "not start of data at %d (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (++this->read_index_ < static_cast<int32_t>(FRAME_SIZE))
|
||||
continue;
|
||||
|
||||
// Full frame received
|
||||
if (validate_data(this->data_)) {
|
||||
this->on_data_(this->data_);
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Invalid frame: try to resync on the next start marker within the buffer
|
||||
log_hex(this->data_, sizeof(this->data_));
|
||||
ESP_LOGE(TAG, "unable to read data");
|
||||
for (int32_t i = 2;
|
||||
i < static_cast<int32_t>(FRAME_STOP_INDEX) && this->read_index_ == static_cast<int32_t>(FRAME_SIZE); ++i) {
|
||||
if ((this->data_[i] == FRAME_START_BYTE_1) && (this->data_[i + 1] == FRAME_START_BYTE_2)) {
|
||||
for (int32_t j = i; j < static_cast<int32_t>(FRAME_SIZE); ++j)
|
||||
this->data_[j - i] = this->data_[j];
|
||||
this->read_index_ = static_cast<int32_t>(FRAME_SIZE) - i;
|
||||
}
|
||||
}
|
||||
if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
|
||||
this->read_index_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ufm01
|
||||
57
esphome/components/ufm01/ufm01.h
Normal file
57
esphome/components/ufm01/ufm01.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
// component API definition at https://www.sciosense.com/wp-content/uploads/2025/06/UFM-01-Datasheet-1.pdf
|
||||
|
||||
namespace esphome::ufm01 {
|
||||
|
||||
static constexpr size_t FRAME_SIZE = 32;
|
||||
|
||||
class UFM01Component : public uart::UARTDevice, public Component {
|
||||
#ifdef USE_SENSOR
|
||||
SUB_SENSOR(accumulated_flow)
|
||||
SUB_SENSOR(flow)
|
||||
SUB_SENSOR(temperature)
|
||||
#endif
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(ufc_chip_error)
|
||||
SUB_BINARY_SENSOR(flow_direction_wrong)
|
||||
SUB_BINARY_SENSOR(empty_tube)
|
||||
SUB_BINARY_SENSOR(flow_rate_out_of_range)
|
||||
#endif
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void loop() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
bool clear_accumulated_flow_();
|
||||
bool set_active_mode_();
|
||||
bool reset_device_();
|
||||
|
||||
private:
|
||||
bool send_command_(const std::array<uint8_t, 7> &command);
|
||||
|
||||
int32_t read_index_ = 0;
|
||||
uint8_t data_[FRAME_SIZE];
|
||||
void on_data_(uint8_t data[FRAME_SIZE]);
|
||||
};
|
||||
|
||||
} // namespace esphome::ufm01
|
||||
30
tests/components/ufm01/common.yaml
Normal file
30
tests/components/ufm01/common.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
ufm01:
|
||||
id: ufm01_component
|
||||
uart_id: uart_bus
|
||||
|
||||
sensor:
|
||||
- platform: ufm01
|
||||
accumulated_flow:
|
||||
id: accumulated_flow
|
||||
name: "Accumulated flow"
|
||||
flow:
|
||||
id: flow
|
||||
name: "Flow"
|
||||
temperature:
|
||||
id: temperature
|
||||
name: "Temperature"
|
||||
|
||||
binary_sensor:
|
||||
- platform: ufm01
|
||||
ufc_chip_error:
|
||||
id: ufc_chip_error
|
||||
name: "UFC chip error"
|
||||
flow_direction_wrong:
|
||||
id: flow_direction_wrong
|
||||
name: "Flow direction wrong"
|
||||
empty_tube:
|
||||
id: empty_tube
|
||||
name: "Empty tube"
|
||||
flow_rate_out_of_range:
|
||||
id: flow_rate_out_of_range
|
||||
name: "Flow rate out of range"
|
||||
4
tests/components/ufm01/test.esp32-idf.yaml
Normal file
4
tests/components/ufm01/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart_2400_even: !include ../../test_build_components/common/uart_2400_even/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/ufm01/test.esp8266-ard.yaml
Normal file
4
tests/components/ufm01/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart_2400_even: !include ../../test_build_components/common/uart_2400_even/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/ufm01/test.rp2040-ard.yaml
Normal file
4
tests/components/ufm01/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart_2400_even: !include ../../test_build_components/common/uart_2400_even/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common UART configuration for ESP32 IDF tests - 2400 baud, EVEN parity
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 2400
|
||||
parity: EVEN
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common UART configuration for ESP8266 Arduino tests - 2400 baud even parity
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 2400
|
||||
parity: EVEN
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common UART configuration for RP2040 Arduino tests - 2400 baud even parity
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO1
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 2400
|
||||
parity: EVEN
|
||||
Reference in New Issue
Block a user