Merge branch 'dev' into sx1509-max6956-apply-outputs

This commit is contained in:
J. Nick Koston authored and GitHub committed 2026-10-30 17:49:55 -10:00
commit 605e7dc475
66 files changed
+1792 -451

No files matched your search

+1
View File
@@ -355,6 +355,7 @@ esphome/components/ld2450/* @hareeshmu
esphome/components/ld2460/* @jesusvallejo
esphome/components/ld24xx/* @kbx81
esphome/components/ld6002b/* @hepter
esphome/components/ld6004/* @wolph
esphome/components/ld600x/* @hepter @wolph
esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2
@@ -35,14 +35,10 @@ bool ATCMiThermometer::parse_device(const ble_device_base::ESPBTDevice &device)
if (!(report_results_(res, addr_str))) {
continue;
}
if (res->temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*res->temperature);
if (res->humidity.has_value() && this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(*res->humidity);
if (res->battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res->battery_level);
if (res->battery_voltage.has_value() && this->battery_voltage_sensor_ != nullptr)
this->battery_voltage_sensor_->publish_state(*res->battery_voltage);
publish_optional(this->temperature_sensor_, res->temperature);
publish_optional(this->humidity_sensor_, res->humidity);
publish_optional(this->battery_level_sensor_, res->battery_level);
publish_optional(this->battery_voltage_sensor_, res->battery_voltage);
success = true;
}
PUBLISH_STATE(this->signal_strength_sensor_, device.get_rssi());
+15 -36
View File
@@ -5,6 +5,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <esphome/components/sensor/sensor.h>
#include <esphome/core/component.h>
@@ -51,40 +52,18 @@ static const uint8_t BME280_MODE_FORCED = 0b01;
static const uint8_t BME280_SOFT_RESET = 0xB6;
static const uint8_t BME280_STATUS_IM_UPDATE = 0b01;
const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT
switch (filter) {
case BME280_IIR_FILTER_OFF:
return "OFF";
case BME280_IIR_FILTER_2X:
return "2x";
case BME280_IIR_FILTER_4X:
return "4x";
case BME280_IIR_FILTER_8X:
return "8x";
case BME280_IIR_FILTER_16X:
return "16x";
default:
return "UNKNOWN";
}
// Indexed by BME280IIRFilter
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "UNKNOWN");
[[maybe_unused]] static const LogString *iir_filter_to_str(BME280IIRFilter value) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(value), IIRFilterStrings::LAST_INDEX);
}
const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT
switch (oversampling) {
case BME280_OVERSAMPLING_NONE:
return "None";
case BME280_OVERSAMPLING_1X:
return "1x";
case BME280_OVERSAMPLING_2X:
return "2x";
case BME280_OVERSAMPLING_4X:
return "4x";
case BME280_OVERSAMPLING_8X:
return "8x";
case BME280_OVERSAMPLING_16X:
return "16x";
default:
return "UNKNOWN";
}
// Indexed by BME280Oversampling
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "1x", "2x", "4x", "8x", "16x", "UNKNOWN");
[[maybe_unused]] static const LogString *oversampling_to_str(BME280Oversampling value) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(value), OversamplingStrings::LAST_INDEX);
}
void BME280Component::setup() {
@@ -176,15 +155,15 @@ void BME280Component::setup() {
}
void BME280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BME280:");
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_)));
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)));
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)));
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->humidity_oversampling_)));
}
inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; }
+15 -42
View File
@@ -2,6 +2,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::bme680 {
@@ -30,46 +31,18 @@ constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0
constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8,
-0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) {
switch (oversampling) {
case BME680_OVERSAMPLING_NONE:
return "None";
case BME680_OVERSAMPLING_1X:
return "1x";
case BME680_OVERSAMPLING_2X:
return "2x";
case BME680_OVERSAMPLING_4X:
return "4x";
case BME680_OVERSAMPLING_8X:
return "8x";
case BME680_OVERSAMPLING_16X:
return "16x";
default:
return "UNKNOWN";
}
// Indexed by BME680Oversampling
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "1x", "2x", "4x", "8x", "16x", "UNKNOWN");
[[maybe_unused]] static const LogString *oversampling_to_str(BME680Oversampling value) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(value), OversamplingStrings::LAST_INDEX);
}
[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) {
switch (filter) {
case BME680_IIR_FILTER_OFF:
return "OFF";
case BME680_IIR_FILTER_1X:
return "1x";
case BME680_IIR_FILTER_3X:
return "3x";
case BME680_IIR_FILTER_7X:
return "7x";
case BME680_IIR_FILTER_15X:
return "15x";
case BME680_IIR_FILTER_31X:
return "31x";
case BME680_IIR_FILTER_63X:
return "63x";
case BME680_IIR_FILTER_127X:
return "127x";
default:
return "UNKNOWN";
}
// Indexed by BME680IIRFilter
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "1x", "3x", "7x", "15x", "31x", "63x", "127x", "UNKNOWN");
[[maybe_unused]] static const LogString *iir_filter_to_str(BME680IIRFilter value) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(value), IIRFilterStrings::LAST_INDEX);
}
void BME680Component::setup() {
@@ -180,15 +153,15 @@ bool BME680Component::setup_registers_() {
void BME680Component::dump_config() {
ESP_LOGCONFIG(TAG, "BME680:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_)));
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)));
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)));
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->humidity_oversampling_)));
LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_);
if (this->heater_duration_ == 0 || this->heater_temperature_ == 0) {
ESP_LOGCONFIG(TAG, " Heater OFF");
+14 -35
View File
@@ -2,6 +2,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::bmp280_base {
@@ -20,40 +21,18 @@ static const uint8_t BMP280_MODE_FORCED = 0b01;
static const uint8_t BMP280_SOFT_RESET = 0xB6;
static const uint8_t BMP280_STATUS_IM_UPDATE = 0b01;
static const char *oversampling_to_str(BMP280Oversampling oversampling) {
switch (oversampling) {
case BMP280_OVERSAMPLING_NONE:
return "None";
case BMP280_OVERSAMPLING_1X:
return "1x";
case BMP280_OVERSAMPLING_2X:
return "2x";
case BMP280_OVERSAMPLING_4X:
return "4x";
case BMP280_OVERSAMPLING_8X:
return "8x";
case BMP280_OVERSAMPLING_16X:
return "16x";
default:
return "UNKNOWN";
}
// Indexed by BMP280Oversampling
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "1x", "2x", "4x", "8x", "16x", "UNKNOWN");
[[maybe_unused]] static const LogString *oversampling_to_str(BMP280Oversampling value) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(value), OversamplingStrings::LAST_INDEX);
}
static const char *iir_filter_to_str(BMP280IIRFilter filter) {
switch (filter) {
case BMP280_IIR_FILTER_OFF:
return "OFF";
case BMP280_IIR_FILTER_2X:
return "2x";
case BMP280_IIR_FILTER_4X:
return "4x";
case BMP280_IIR_FILTER_8X:
return "8x";
case BMP280_IIR_FILTER_16X:
return "16x";
default:
return "UNKNOWN";
}
// Indexed by BMP280IIRFilter
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "UNKNOWN");
[[maybe_unused]] static const LogString *iir_filter_to_str(BMP280IIRFilter value) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(value), IIRFilterStrings::LAST_INDEX);
}
void BMP280Component::setup() {
@@ -124,13 +103,13 @@ void BMP280Component::setup() {
}
void BMP280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMP280:");
ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_));
ESP_LOGCONFIG(TAG, " IIR Filter: %s", LOG_STR_ARG(iir_filter_to_str(this->iir_filter_)));
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->temperature_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)));
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)));
}
inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; }
+6 -8
View File
@@ -33,9 +33,8 @@ void GCJA5Component::loop() {
// check if rx_message_ has 32 bytes of data
if (this->rx_message_.size() == 32) {
this->parse_data_();
if (this->have_good_data_) {
if (this->parse_data_()) {
this->status_clear_warning();
PUBLISH_STATE(this->pm_1_0_sensor_, get_32_bit_uint_(1));
PUBLISH_STATE(this->pm_2_5_sensor_, get_32_bit_uint_(5));
PUBLISH_STATE(this->pm_10_0_sensor_, get_32_bit_uint_(9));
@@ -64,7 +63,7 @@ bool GCJA5Component::calculate_checksum_() {
return (crc == this->rx_message_[30]);
}
void GCJA5Component::parse_data_() {
bool GCJA5Component::parse_data_() {
ESP_LOGVV(TAG, "GCJA5 Data: ");
for (uint8_t i = 0; i < 32; i++) {
ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->rx_message_[i]),
@@ -73,12 +72,10 @@ void GCJA5Component::parse_data_() {
if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) {
ESP_LOGVV(TAG, "Discarding bad packet - failed checks.");
return;
} else {
ESP_LOGVV(TAG, "Good packet found.");
return false;
}
ESP_LOGVV(TAG, "Good packet found.");
this->have_good_data_ = true;
uint8_t status = this->rx_message_[29];
if (!this->first_status_log_) {
this->first_status_log_ = true;
@@ -91,6 +88,7 @@ void GCJA5Component::parse_data_() {
" Fan Status : %i",
(status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03);
}
return true;
}
void GCJA5Component::dump_config() { ; }
+1 -2
View File
@@ -23,7 +23,7 @@ class GCJA5Component final : public Component, public uart::UARTDevice {
void loop() override;
protected:
void parse_data_();
bool parse_data_();
bool calculate_checksum_();
uint16_t get_16_bit_uint_(uint8_t start_index) const {
@@ -36,7 +36,6 @@ class GCJA5Component final : public Component, public uart::UARTDevice {
uint32_t last_transmission_{0};
std::vector<uint8_t> rx_message_;
bool have_good_data_{false};
bool first_status_log_{false};
};
+6 -6
View File
@@ -174,15 +174,15 @@ void LD6002BComponent::wake_() {
}
#ifdef USE_SWITCH
void LD6002BComponent::set_switch_state(switch_::Switch *entity, bool state) {
if (entity != this->low_power_switch_) {
this->LD600XComponent::set_switch_state(entity, state);
return;
}
bool LD6002BComponent::set_switch_state(switch_::Switch *entity, bool state) {
if (entity != this->low_power_switch_)
return this->LD600XComponent::set_switch_state(entity, state);
if (!this->send_control_command_(state ? CMD_LOW_POWER_ON : CMD_LOW_POWER_OFF))
return false;
this->low_power_enabled_ = state;
this->low_power_reported_ = true;
this->send_control_command_(state ? CMD_LOW_POWER_ON : CMD_LOW_POWER_OFF);
this->update_work_mode_fallback_();
return true;
}
#endif
+1 -1
View File
@@ -21,7 +21,7 @@ class LD6002BComponent final : public ld600x::LD600XComponent {
#ifdef USE_SWITCH
SUB_SWITCH(low_power)
void set_switch_state(switch_::Switch *entity, bool state) override;
bool set_switch_state(switch_::Switch *entity, bool state) override;
#endif
#ifdef USE_BUTTON
void press_button(uint8_t kind) override;
+27
View File
@@ -0,0 +1,27 @@
import esphome.codegen as cg
from esphome.components import uart
from esphome.components.ld600x import (
LD600XComponent,
ld600x_hub_schema,
request_model_sizes,
uart_final_validate,
)
from esphome.types import ConfigType
AUTO_LOAD = ["ld600x"]
CODEOWNERS = ["@wolph"]
DEPENDENCIES = ["uart"]
DOMAIN = "ld6004"
MULTI_CONF = True
ld6004_ns = cg.esphome_ns.namespace("ld6004")
LD6004Component = ld6004_ns.class_("LD6004Component", LD600XComponent)
CONFIG_SCHEMA = ld600x_hub_schema(LD6004Component)
FINAL_VALIDATE_SCHEMA = uart_final_validate("ld6004")
async def to_code(config: ConfigType) -> None:
var = await uart.new_uart_component(config)
# The dwell areas take a third kind, which the platforms request only when they use them.
request_model_sizes(var, area_kinds=2)
@@ -0,0 +1,13 @@
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from . import LD6004Component
from .const import CONF_LD6004_ID
DEPENDENCIES = ["ld6004"]
CONFIG_SCHEMA = entities.binary_sensor_schema(LD6004Component, CONF_LD6004_ID)
async def to_code(config: ConfigType) -> None:
await entities.binary_sensor_to_code(config, CONF_LD6004_ID)
+2
View File
@@ -0,0 +1,2 @@
AREA_KINDS = ("interference", "detection", "dwell")
CONF_LD6004_ID = "ld6004_id"
+197
View File
@@ -0,0 +1,197 @@
#include "ld6004.h"
#include "esphome/components/ld600x/ld600x_frame.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cmath>
namespace esphome::ld6004 {
ESPHOME_LOG_TAG(TAG, "ld6004");
static constexpr uint16_t TYPE_SET_DWELL_LIFETIME = 0x0206;
static constexpr uint16_t TYPE_SET_OUTPUT_INTERVAL = 0x0207;
static constexpr uint16_t TYPE_REPORT_TARGET = 0x0A04;
static constexpr uint16_t TYPE_REPORT_AREA_PRESENCE = 0x0A0A;
static constexpr uint16_t TYPE_REPORT_WORK_MODE = 0x0A12;
static constexpr uint16_t TYPE_REPORT_WORK_STATUS = 0x0A14;
static constexpr uint16_t TYPE_REPORT_P20_MODE = 0x0A15;
static constexpr uint16_t TYPE_REPORT_DWELL_LIFETIME = 0x0A17;
static constexpr uint16_t TYPE_REPORT_OUTPUT_INTERVAL = 0x0A18;
static constexpr uint32_t CMD_WORK_LOW_POWER = 0x16;
static constexpr uint32_t CMD_WORK_NORMAL = 0x17;
static constexpr uint32_t CMD_GET_WORK_MODE = 0x18;
static constexpr uint32_t CMD_WORK_RADAR_OFF_P20_HIGH = 0x1B;
static constexpr uint32_t CMD_WORK_RADAR_OFF_P20_LOW = 0x1C;
static constexpr uint32_t CMD_P20_PRESENCE_HIGH = 0x1D;
static constexpr uint32_t CMD_P20_PRESENCE_LOW = 0x1E;
static constexpr uint32_t CMD_GET_P20_MODE = 0x1F;
static constexpr uint32_t CMD_P20_CONSTANT_LOW = 0x20;
static constexpr uint32_t CMD_P20_CONSTANT_HIGH = 0x21;
static constexpr uint32_t CMD_P20_PULSE_LOW = 0x22;
static constexpr uint32_t CMD_P20_PULSE_HIGH = 0x23;
static constexpr uint32_t CMD_WORK_HIGH_REFLECTIVITY = 0x24;
static constexpr uint32_t CMD_CLEAR_DWELL = 0x25;
static constexpr uint32_t CMD_GET_DWELL_LIFETIME = 0x26;
static constexpr uint32_t CMD_GET_OUTPUT_INTERVAL = 0x27;
// The work_mode select options, the 0x0A12 report values and the tables below share one order.
static constexpr uint8_t WORK_MODE_COUNT = 5;
static constexpr uint8_t WORK_MODE_RADAR_OFF_P20_HIGH = 2;
static constexpr uint8_t WORK_MODE_RADAR_OFF_P20_LOW = 3;
static constexpr uint8_t P20_MODE_COUNT = 6;
#ifdef USE_SELECT
// Every control code fits in a byte, so the tables are bytes in flash.
static constexpr uint8_t WORK_MODE_COMMANDS[WORK_MODE_COUNT] PROGMEM = {
CMD_WORK_NORMAL, CMD_WORK_LOW_POWER, CMD_WORK_RADAR_OFF_P20_HIGH, CMD_WORK_RADAR_OFF_P20_LOW,
CMD_WORK_HIGH_REFLECTIVITY};
static constexpr uint8_t P20_MODE_COMMANDS[P20_MODE_COUNT] PROGMEM = {CMD_P20_PRESENCE_HIGH, CMD_P20_PRESENCE_LOW,
CMD_P20_CONSTANT_LOW, CMD_P20_CONSTANT_HIGH,
CMD_P20_PULSE_LOW, CMD_P20_PULSE_HIGH};
#endif
#ifdef USE_TEXT_SENSOR
PROGMEM_STRING_TABLE(WorkModeStrings, "normal", "low_power", "radar_off_p20_high", "radar_off_p20_low",
"high_reflectivity");
static_assert(WorkModeStrings::COUNT == WORK_MODE_COUNT);
#endif
bool LD6004Component::handle_model_report(uint16_t type, const uint8_t *data, uint16_t len) {
switch (type) {
case TYPE_REPORT_WORK_MODE: {
if (len != 1 && len != 4)
break;
const uint32_t mode = len == 4 ? encode_value_le<uint32_t>(data) : data[0];
if (mode >= WORK_MODE_COUNT)
break;
this->radar_off_ = mode == WORK_MODE_RADAR_OFF_P20_HIGH || mode == WORK_MODE_RADAR_OFF_P20_LOW;
if (this->radar_off_) {
// Detection has stopped, so no later report clears what the last ones showed.
this->clear_area_presence_();
this->clear_target_state_();
}
#ifdef USE_SELECT
PUBLISH_STATE(this->work_mode_select_, mode);
#endif
#ifdef USE_TEXT_SENSOR
if (this->work_mode_text_sensor_ != nullptr)
this->work_mode_text_sensor_->publish_state(WorkModeStrings::get_progmem_str(static_cast<uint8_t>(mode), 0));
#endif
return true;
}
case TYPE_REPORT_TARGET:
case TYPE_REPORT_AREA_PRESENCE:
// Claimed while the radar is off: frames still in flight must not restore the cleared presence.
return this->radar_off_;
case TYPE_REPORT_WORK_STATUS:
// 0x0A14 marks the unattended low-power transition; a zero means nobody is in any area.
if (len >= 1 && data[0] == 0 && !this->target_presence_any_)
this->clear_area_presence_();
return true;
case TYPE_REPORT_P20_MODE:
if (len != 1 || data[0] >= P20_MODE_COUNT)
break;
#ifdef USE_SELECT
PUBLISH_STATE(this->p20_mode_select_, data[0]);
#endif
return true;
case TYPE_REPORT_DWELL_LIFETIME:
if (len != 4)
break;
#ifdef USE_NUMBER
this->publish_number_clamped_(this->dwell_lifetime_number_, encode_value_le<uint32_t>(data));
#endif
return true;
case TYPE_REPORT_OUTPUT_INTERVAL:
if (len != 4)
break;
#ifdef USE_NUMBER
this->publish_number_clamped_(this->output_interval_number_, encode_value_le<uint32_t>(data));
#endif
return true;
default:
return false;
}
// Still claimed, so the base does not treat it as an unknown frame.
ESP_LOGD(TAG, "Unexpected report 0x%04X: %u bytes, first byte 0x%02X", type, len, len > 0 ? data[0] : 0);
return true;
}
#ifdef USE_NUMBER
void LD6004Component::set_number_value(number::Number *entity, float value) {
if (entity != this->dwell_lifetime_number_ && entity != this->output_interval_number_) {
this->LD600XComponent::set_number_value(entity, value);
return;
}
// NumberCall already keeps the value inside the traits' range; the module takes whole numbers only.
if (std::floor(value) != value) {
ESP_LOGW(TAG, "Invalid number value: %f", value);
return;
}
uint8_t data[4];
ld600x::write_u32_le(data, static_cast<uint32_t>(value));
this->queue_command_(entity == this->dwell_lifetime_number_ ? TYPE_SET_DWELL_LIFETIME : TYPE_SET_OUTPUT_INTERVAL,
data, sizeof(data));
}
#endif // USE_NUMBER
#ifdef USE_SELECT
void LD6004Component::set_select_value(select::Select *entity, size_t index) {
if (entity == this->work_mode_select_) {
if (index < WORK_MODE_COUNT)
this->send_control_command_(progmem_read_byte(&WORK_MODE_COMMANDS[index]));
} else if (entity == this->p20_mode_select_) {
if (index < P20_MODE_COUNT)
this->send_control_command_(progmem_read_byte(&P20_MODE_COMMANDS[index]));
} else {
this->LD600XComponent::set_select_value(entity, index);
}
}
#endif // USE_SELECT
#ifdef USE_BUTTON
void LD6004Component::press_button(uint8_t kind) {
if (kind == BUTTON_CLEAR_DWELL) {
if (this->send_control_command_(CMD_CLEAR_DWELL)) {
this->LD600XComponent::press_button(ld600x::BUTTON_GET_AREAS);
}
} else {
this->LD600XComponent::press_button(kind);
}
}
#endif // USE_BUTTON
void LD6004Component::setup_model() {
bool want_work_mode = false;
#ifdef USE_SELECT
want_work_mode = this->work_mode_select_ != nullptr;
if (this->p20_mode_select_ != nullptr)
this->send_control_command_(CMD_GET_P20_MODE);
#endif
#ifdef USE_TEXT_SENSOR
// The text sensor follows the 0x0A12 report too, so it needs the same boot query as the select
// to show a mode before the module next changes it.
want_work_mode = want_work_mode || this->work_mode_text_sensor_ != nullptr;
#endif
if (want_work_mode)
this->send_control_command_(CMD_GET_WORK_MODE);
#ifdef USE_NUMBER
if (this->dwell_lifetime_number_ != nullptr)
this->send_control_command_(CMD_GET_DWELL_LIFETIME);
if (this->output_interval_number_ != nullptr)
this->send_control_command_(CMD_GET_OUTPUT_INTERVAL);
#endif
}
void LD6004Component::dump_model_config() {
#ifdef USE_NUMBER
LOG_NUMBER(" ", "Dwell Lifetime", this->dwell_lifetime_number_);
LOG_NUMBER(" ", "Output Interval", this->output_interval_number_);
#endif
#ifdef USE_SELECT
LOG_SELECT(" ", "Work Mode", this->work_mode_select_);
LOG_SELECT(" ", "P20 Mode", this->p20_mode_select_);
#endif
}
} // namespace esphome::ld6004
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "esphome/components/ld600x/ld600x.h"
namespace esphome::ld6004 {
enum LD6004ButtonType : uint8_t {
BUTTON_CLEAR_DWELL = ld600x::LD600X_MODEL_KIND_BASE,
};
class LD6004Component final : public ld600x::LD600XComponent {
public:
LD6004Component() : LD600XComponent(LOG_STR("ld6004")) {}
#ifdef USE_NUMBER
SUB_NUMBER(dwell_lifetime)
SUB_NUMBER(output_interval)
#endif
#ifdef USE_SELECT
SUB_SELECT(work_mode)
SUB_SELECT(p20_mode)
#endif
#ifdef USE_NUMBER
void set_number_value(number::Number *entity, float value) override;
#endif
#ifdef USE_SELECT
void set_select_value(select::Select *entity, size_t index) override;
#endif
#ifdef USE_BUTTON
void press_button(uint8_t kind) override;
#endif
protected:
bool handle_model_report(uint16_t type, const uint8_t *data, uint16_t len) override;
void setup_model() override;
void dump_model_config() override;
// Set by a work mode report that turns the radar off, and cleared by one that turns it back on.
bool radar_off_{false};
};
} // namespace esphome::ld6004
+15
View File
@@ -0,0 +1,15 @@
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from . import LD6004Component
from .const import AREA_KINDS, CONF_LD6004_ID
DEPENDENCIES = ["ld6004"]
CONFIG_SCHEMA = entities.sensor_schema(
LD6004Component, CONF_LD6004_ID, area_kinds=AREA_KINDS
)
async def to_code(config: ConfigType) -> None:
await entities.sensor_to_code(config, CONF_LD6004_ID, area_kinds=AREA_KINDS)
+29 -33
View File
@@ -39,7 +39,7 @@ from esphome.cpp_generator import MockObj, MockObjClass, ParameterListExpression
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import AreaAxis, AreaKind, ButtonType
from . import AreaAxis, AreaKind, ButtonType, request_model_sizes
from .const import (
AREA_COUNT,
CONF_APPLY_AREA,
@@ -196,14 +196,19 @@ async def sensor_to_code(
if conf := target_config.get(key):
cg.add(setter(i, await sensor.new_sensor(conf)))
for kind in area_kinds:
used_kinds = 0
for kind_index, kind in enumerate(area_kinds):
area_kind = _AREA_KINDS[kind]
for i in range(AREA_COUNT):
if area_config := config.get(f"{kind}_area_{i}"):
used_kinds = kind_index + 1
for key, axis in _AREA_AXES:
if conf := area_config.get(key):
sens = await sensor.new_sensor(conf)
cg.add(hub.set_area_sensor(area_kind, i, axis, sens))
# The hub sizes the common kinds; a later kind such as dwell is sized only when used.
if used_kinds:
request_model_sizes(hub, area_kinds=used_kinds)
_OCCUPANCY_SCHEMA = binary_sensor.binary_sensor_schema(
@@ -425,21 +430,22 @@ async def select_to_code(
return selects
CONFIG_SWITCH_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
)
_SWITCH_SCHEMAS = {
CONF_POINT_CLOUD: CONFIG_SWITCH_SCHEMA,
CONF_TARGET_DISPLAY: switch.switch_schema(
def _config_switch_schema(**kwargs: Any) -> cv.Schema:
return switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="RESTORE_DEFAULT_ON",
**kwargs,
)
CONFIG_SWITCH_SCHEMA = _config_switch_schema()
_SWITCH_SCHEMAS = {
CONF_POINT_CLOUD: CONFIG_SWITCH_SCHEMA,
CONF_TARGET_DISPLAY: _config_switch_schema(
default_restore_mode="RESTORE_DEFAULT_ON"
),
}
@@ -448,47 +454,42 @@ def switch_schema(
hub_class: MockObjClass,
hub_id_key: str,
*,
keys: tuple[str, ...] | None = None,
extra: dict[Any, Any] | None = None,
) -> cv.Schema:
return cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
**{
cv.Optional(key): schema
for key, schema in _SWITCH_SCHEMAS.items()
if keys is None or key in keys
},
**{cv.Optional(key): schema for key, schema in _SWITCH_SCHEMAS.items()},
}
).extend(extra or {})
SWITCH_CONTROL: dict[str, str] = {
"apply": "set_switch_state(entity, state)",
"publish": "after",
# A full command queue rejects the write, and the switch then keeps its old state.
"publish": "on_success",
}
async def switch_to_code(config: ConfigType, hub_id_key: str) -> None:
hub = await cg.get_variable(config[hub_id_key])
# The schema only accepts the model's keys, so a missing one is skipped here.
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_POINT_CLOUD, hub.set_point_cloud_switch, **SWITCH_CONTROL)
await switches(CONF_TARGET_DISPLAY, hub.set_target_display_switch, **SWITCH_CONTROL)
_CONFIG_BUTTON_SCHEMA = button.button_schema(
CONFIG_BUTTON_SCHEMA = button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
)
DIAGNOSTIC_BUTTON_SCHEMA = button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
)
_BUTTON_SCHEMAS = {
CONF_APPLY_AREA: _CONFIG_BUTTON_SCHEMA,
CONF_AUTO_INTERFERENCE: _CONFIG_BUTTON_SCHEMA,
CONF_APPLY_AREA: CONFIG_BUTTON_SCHEMA,
CONF_AUTO_INTERFERENCE: CONFIG_BUTTON_SCHEMA,
CONF_GET_AREAS: DIAGNOSTIC_BUTTON_SCHEMA,
CONF_CLEAR_INTERFERENCE: _CONFIG_BUTTON_SCHEMA,
CONF_RESET_DETECTION_AREA: _CONFIG_BUTTON_SCHEMA,
CONF_CLEAR_INTERFERENCE: CONFIG_BUTTON_SCHEMA,
CONF_RESET_DETECTION_AREA: CONFIG_BUTTON_SCHEMA,
CONF_GET_DELAY: DIAGNOSTIC_BUTTON_SCHEMA,
CONF_GET_SENSITIVITY: DIAGNOSTIC_BUTTON_SCHEMA,
CONF_GET_TRIGGER_SPEED: DIAGNOSTIC_BUTTON_SCHEMA,
@@ -496,7 +497,7 @@ _BUTTON_SCHEMAS = {
CONF_GET_INSTALLATION: DIAGNOSTIC_BUTTON_SCHEMA,
CONF_GET_LOW_POWER_MODE: DIAGNOSTIC_BUTTON_SCHEMA,
CONF_GET_LOW_POWER_SLEEP_TIME: DIAGNOSTIC_BUTTON_SCHEMA,
CONF_RESET_UNATTENDED: _CONFIG_BUTTON_SCHEMA,
CONF_RESET_UNATTENDED: CONFIG_BUTTON_SCHEMA,
}
@@ -504,17 +505,12 @@ def button_schema(
hub_class: MockObjClass,
hub_id_key: str,
*,
keys: tuple[str, ...] | None = None,
extra: dict[Any, Any] | None = None,
) -> cv.Schema:
return cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
**{
cv.Optional(key): schema
for key, schema in _BUTTON_SCHEMAS.items()
if keys is None or key in keys
},
**{cv.Optional(key): schema for key, schema in _BUTTON_SCHEMAS.items()},
}
).extend(extra or {})
+6 -3
View File
@@ -1380,10 +1380,11 @@ void LD600XComponent::clear_target_state_() {
}
#ifdef USE_SWITCH
void LD600XComponent::set_switch_state(switch_::Switch *entity, bool state) {
bool LD600XComponent::set_switch_state(switch_::Switch *entity, bool state) {
if (entity == this->point_cloud_switch_) {
if (!this->send_control_command_(state ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF))
return false;
this->point_cloud_enabled_ = state;
this->send_control_command_(state ? CMD_POINT_CLOUD_ON : CMD_POINT_CLOUD_OFF);
#ifdef USE_SENSOR
// The count only moves while the stream runs, so the last one would stand as
// a live reading. The dedup sentinel is cleared with it: the same count is
@@ -1392,13 +1393,15 @@ void LD600XComponent::set_switch_state(switch_::Switch *entity, bool state) {
this->point_count_sensor_.publish_state_unknown();
#endif
} else { // Only registered entities call this, so the last one needs no compare
if (!this->send_control_command_(state ? CMD_TARGET_DISPLAY_ON : CMD_TARGET_DISPLAY_OFF))
return false;
this->target_display_enabled_ = state;
this->send_control_command_(state ? CMD_TARGET_DISPLAY_ON : CMD_TARGET_DISPLAY_OFF);
if (!state) {
// Every target entity is fed by the reports this just stopped.
this->clear_target_state_();
}
}
return true;
}
#endif
+2 -1
View File
@@ -201,7 +201,8 @@ class LD600XComponent : public Component, public uart::UARTDevice {
virtual void set_select_value(select::Select *entity, size_t index);
#endif
#ifdef USE_SWITCH
virtual void set_switch_state(switch_::Switch *entity, bool state);
// Returns false when the command was not queued, so the switch keeps its old state.
virtual bool set_switch_state(switch_::Switch *entity, bool state);
#endif
#ifdef USE_BUTTON
virtual void press_button(uint8_t kind);
@@ -35,14 +35,10 @@ bool PVVXMiThermometer::parse_device(const ble_device_base::ESPBTDevice &device)
if (!(report_results_(res, addr_str))) {
continue;
}
if (res->temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*res->temperature);
if (res->humidity.has_value() && this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(*res->humidity);
if (res->battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res->battery_level);
if (res->battery_voltage.has_value() && this->battery_voltage_sensor_ != nullptr)
this->battery_voltage_sensor_->publish_state(*res->battery_voltage);
publish_optional(this->temperature_sensor_, res->temperature);
publish_optional(this->humidity_sensor_, res->humidity);
publish_optional(this->battery_level_sensor_, res->battery_level);
publish_optional(this->battery_voltage_sensor_, res->battery_voltage);
PUBLISH_STATE(this->signal_strength_sensor_, device.get_rssi());
success = true;
}
+15 -40
View File
@@ -1,4 +1,5 @@
#include "qmp6988.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
#include <cmath>
@@ -45,46 +46,18 @@ static const uint32_t SUBTRACTOR = 8388608;
ESPHOME_LOG_TAG(TAG, "qmp6988");
static const char *oversampling_to_str(QMP6988Oversampling oversampling) {
switch (oversampling) {
case QMP6988_OVERSAMPLING_SKIPPED:
return "None";
case QMP6988_OVERSAMPLING_1X:
return "1x";
case QMP6988_OVERSAMPLING_2X:
return "2x";
case QMP6988_OVERSAMPLING_4X:
return "4x";
case QMP6988_OVERSAMPLING_8X:
return "8x";
case QMP6988_OVERSAMPLING_16X:
return "16x";
case QMP6988_OVERSAMPLING_32X:
return "32x";
case QMP6988_OVERSAMPLING_64X:
return "64x";
default:
return "UNKNOWN";
}
// Indexed by QMP6988Oversampling
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "1x", "2x", "4x", "8x", "16x", "32x", "64x", "UNKNOWN");
[[maybe_unused]] static const LogString *oversampling_to_str(QMP6988Oversampling value) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(value), OversamplingStrings::LAST_INDEX);
}
static const char *iir_filter_to_str(QMP6988IIRFilter filter) {
switch (filter) {
case QMP6988_IIR_FILTER_OFF:
return "OFF";
case QMP6988_IIR_FILTER_2X:
return "2x";
case QMP6988_IIR_FILTER_4X:
return "4x";
case QMP6988_IIR_FILTER_8X:
return "8x";
case QMP6988_IIR_FILTER_16X:
return "16x";
case QMP6988_IIR_FILTER_32X:
return "32x";
default:
return "UNKNOWN";
}
// Indexed by QMP6988IIRFilter
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "UNKNOWN");
[[maybe_unused]] static const LogString *iir_filter_to_str(QMP6988IIRFilter value) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(value), IIRFilterStrings::LAST_INDEX);
}
bool QMP6988Component::device_check_() {
@@ -327,12 +300,14 @@ void QMP6988Component::dump_config() {
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
ESP_LOGCONFIG(TAG, " Temperature Oversampling: %s", oversampling_to_str(this->temperature_oversampling_));
ESP_LOGCONFIG(TAG, " Temperature Oversampling: %s",
LOG_STR_ARG(oversampling_to_str(this->temperature_oversampling_)));
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
ESP_LOGCONFIG(TAG,
" Pressure Oversampling: %s\n"
" IIR Filter: %s",
oversampling_to_str(this->pressure_oversampling_), iir_filter_to_str(this->iir_filter_));
LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)),
LOG_STR_ARG(iir_filter_to_str(this->iir_filter_)));
}
void QMP6988Component::update() {
+11 -22
View File
@@ -31,28 +31,17 @@ class RuuviTag final : public Component, public ble_device_base::ESPBTDeviceList
if (!res.has_value())
return false;
if (res->humidity.has_value() && this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(*res->humidity);
if (res->temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*res->temperature);
if (res->pressure.has_value() && this->pressure_sensor_ != nullptr)
this->pressure_sensor_->publish_state(*res->pressure);
if (res->acceleration.has_value() && this->acceleration_sensor_ != nullptr)
this->acceleration_sensor_->publish_state(*res->acceleration);
if (res->acceleration_x.has_value() && this->acceleration_x_sensor_ != nullptr)
this->acceleration_x_sensor_->publish_state(*res->acceleration_x);
if (res->acceleration_y.has_value() && this->acceleration_y_sensor_ != nullptr)
this->acceleration_y_sensor_->publish_state(*res->acceleration_y);
if (res->acceleration_z.has_value() && this->acceleration_z_sensor_ != nullptr)
this->acceleration_z_sensor_->publish_state(*res->acceleration_z);
if (res->battery_voltage.has_value() && this->battery_voltage_sensor_ != nullptr)
this->battery_voltage_sensor_->publish_state(*res->battery_voltage);
if (res->tx_power.has_value() && this->tx_power_sensor_ != nullptr)
this->tx_power_sensor_->publish_state(*res->tx_power);
if (res->movement_counter.has_value() && this->movement_counter_sensor_ != nullptr)
this->movement_counter_sensor_->publish_state(*res->movement_counter);
if (res->measurement_sequence_number.has_value() && this->measurement_sequence_number_sensor_ != nullptr)
this->measurement_sequence_number_sensor_->publish_state(*res->measurement_sequence_number);
publish_optional(this->humidity_sensor_, res->humidity);
publish_optional(this->temperature_sensor_, res->temperature);
publish_optional(this->pressure_sensor_, res->pressure);
publish_optional(this->acceleration_sensor_, res->acceleration);
publish_optional(this->acceleration_x_sensor_, res->acceleration_x);
publish_optional(this->acceleration_y_sensor_, res->acceleration_y);
publish_optional(this->acceleration_z_sensor_, res->acceleration_z);
publish_optional(this->battery_voltage_sensor_, res->battery_voltage);
publish_optional(this->tx_power_sensor_, res->tx_power);
publish_optional(this->movement_counter_sensor_, res->movement_counter);
publish_optional(this->measurement_sequence_number_sensor_, res->measurement_sequence_number);
return true;
}
+7 -7
View File
@@ -30,12 +30,12 @@ SM10BIT_BASE_CONFIG_SCHEMA = cv.Schema(
async def register_sm10bit_base(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
var = cg.new_Pvariable(
config[CONF_ID],
await cg.gpio_pin_expression(config[CONF_DATA_PIN]),
await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]),
config[CONF_MAX_POWER_COLOR_CHANNELS],
config[CONF_MAX_POWER_WHITE_CHANNELS],
)
await cg.register_component(var, config)
await cg.add_pin(var.set_data_pin, config[CONF_DATA_PIN])
await cg.add_pin(var.set_clock_pin, config[CONF_CLOCK_PIN])
cg.add(var.set_max_power_color_channels(config[CONF_MAX_POWER_COLOR_CHANNELS]))
cg.add(var.set_max_power_white_channels(config[CONF_MAX_POWER_WHITE_CHANNELS]))
return var
@@ -1,16 +1,15 @@
#include "sm10bit_base.h"
#include "esphome/core/log.h"
namespace esphome::sm10bit_base {
ESPHOME_LOG_TAG(TAG, "sm10bit_base");
static constexpr uint8_t SM10BIT_MODEL_ID = 0xC0;
static const uint8_t SM10BIT_ADDR_STANDBY = 0x0;
static const uint8_t SM10BIT_ADDR_START_3CH = 0x8;
static const uint8_t SM10BIT_ADDR_START_2CH = 0x10;
static const uint8_t SM10BIT_ADDR_START_5CH = 0x18;
static constexpr uint8_t SM10BIT_ADDR_STANDBY = 0x0;
static constexpr uint8_t SM10BIT_ADDR_START_3CH = 0x8;
static constexpr uint8_t SM10BIT_ADDR_START_2CH = 0x10;
static constexpr uint8_t SM10BIT_ADDR_START_5CH = 0x18;
static const uint8_t SM10BIT_DELAY = 2;
static constexpr uint8_t SM10BIT_DELAY = 2;
// Power current values
// HEX | Binary | RGB level | White level | Config value
@@ -31,63 +30,50 @@ static const uint8_t SM10BIT_DELAY = 2;
// 0xE | 1110 | RGB 150mA | CW 75mA | 14
// 0xF | 1111 | RGB 160mA | CW 80mA | 15
void Sm10BitBase::loop() {
if (!this->update_)
return;
void Sm10BitBase::setup() {
this->data_pin_->setup();
this->data_pin_->digital_write(true);
this->clock_pin_->setup();
this->clock_pin_->digital_write(true);
}
uint8_t data[12];
if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
for (int i = 1; i < 12; i++)
data[i] = 0;
// First turn all channels off
data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH;
this->write_buffer_(data, 12);
// Then sleep
data[0] = this->model_id_ + SM10BIT_ADDR_STANDBY;
this->write_buffer_(data, 12);
} else if (this->pwm_amounts_[0] == 0 && this->pwm_amounts_[1] == 0 && this->pwm_amounts_[2] == 0 &&
(this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0)) {
// Only data on white channels
data[0] = this->model_id_ + SM10BIT_ADDR_START_2CH;
data[1] = 0 << 4 | this->max_power_white_channels_;
for (int i = 2, j = 0; i < 12; i += 2, j++) {
data[i] = this->pwm_amounts_[j] >> 0x8;
data[i + 1] = this->pwm_amounts_[j] & 0xFF;
}
this->write_buffer_(data, 12);
} else if ((this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0) &&
this->pwm_amounts_[3] == 0 && this->pwm_amounts_[4] == 0) {
// Only data on RGB channels
data[0] = this->model_id_ + SM10BIT_ADDR_START_3CH;
data[1] = this->max_power_color_channels_ << 4 | 0;
for (int i = 2, j = 0; i < 12; i += 2, j++) {
data[i] = this->pwm_amounts_[j] >> 0x8;
data[i + 1] = this->pwm_amounts_[j] & 0xFF;
}
this->write_buffer_(data, 12);
} else {
// All channels
data[0] = this->model_id_ + SM10BIT_ADDR_START_5CH;
data[1] = this->max_power_color_channels_ << 4 | this->max_power_white_channels_;
for (int i = 2, j = 0; i < 12; i += 2, j++) {
data[i] = this->pwm_amounts_[j] >> 0x8;
data[i + 1] = this->pwm_amounts_[j] & 0xFF;
}
this->write_buffer_(data, 12);
void Sm10BitBase::loop() {
this->disable_loop();
const bool color_on = this->pwm_amounts_[0] > 0 || this->pwm_amounts_[1] > 0 || this->pwm_amounts_[2] > 0;
const bool white_on = this->pwm_amounts_[3] > 0 || this->pwm_amounts_[4] > 0;
uint8_t addr = SM10BIT_ADDR_START_5CH;
if (color_on && !white_on) {
addr = SM10BIT_ADDR_START_3CH;
} else if (white_on && !color_on) {
addr = SM10BIT_ADDR_START_2CH;
}
this->update_ = false;
uint8_t data[12]{};
data[0] = SM10BIT_MODEL_ID + addr;
// Only the max power of the channel groups being driven is sent
data[1] = (color_on ? this->max_power_color_channels_ << 4 : 0) | (white_on ? this->max_power_white_channels_ : 0);
for (size_t i = 0; i < this->pwm_amounts_.size(); i++) {
data[2 + i * 2] = this->pwm_amounts_[i] >> 8;
data[3 + i * 2] = this->pwm_amounts_[i] & 0xFF;
}
this->write_buffer_(data, sizeof(data));
if (!color_on && !white_on) {
// All channels are off, so put the chip to sleep
data[0] = SM10BIT_MODEL_ID + SM10BIT_ADDR_STANDBY;
this->write_buffer_(data, sizeof(data));
}
}
void Sm10BitBase::write_channel(uint8_t channel, float state) {
auto value = static_cast<uint16_t>(state * 0x3FF);
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
this->pwm_amounts_[channel] = value;
this->enable_loop();
}
this->pwm_amounts_[channel] = value;
}
void Sm10BitBase::write_bit_(bool value) {
this->data_pin_->digital_write(value);
delayMicroseconds(SM10BIT_DELAY);
@@ -111,7 +97,7 @@ void Sm10BitBase::write_byte_(uint8_t data) {
this->data_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
void Sm10BitBase::write_buffer_(uint8_t *buffer, uint8_t size) {
void Sm10BitBase::write_buffer_(const uint8_t *buffer, uint8_t size) {
this->data_pin_->digital_write(false);
delayMicroseconds(SM10BIT_DELAY);
this->clock_pin_->digital_write(false);
+13 -20
View File
@@ -2,26 +2,22 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include <vector>
#include <array>
namespace esphome::sm10bit_base {
class Sm10BitBase : public Component {
public:
void set_model(uint8_t model_id) { model_id_ = model_id; }
void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
void set_max_power_color_channels(uint8_t max_power_color_channels) {
max_power_color_channels_ = max_power_color_channels;
}
void set_max_power_white_channels(uint8_t max_power_white_channels) {
max_power_white_channels_ = max_power_white_channels;
}
Sm10BitBase(GPIOPin *data_pin, GPIOPin *clock_pin, uint8_t max_power_color_channels, uint8_t max_power_white_channels)
: data_pin_(data_pin),
clock_pin_(clock_pin),
max_power_color_channels_(max_power_color_channels),
max_power_white_channels_(max_power_white_channels) {}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void setup() override;
void dump_config() override;
/// Send the new duty values, then sleep until write_channel() changes one.
void loop() override;
void write_channel(uint8_t channel, float state);
@@ -29,16 +25,13 @@ class Sm10BitBase : public Component {
protected:
void write_bit_(bool value);
void write_byte_(uint8_t data);
void write_buffer_(uint8_t *buffer, uint8_t size);
void write_buffer_(const uint8_t *buffer, uint8_t size);
GPIOPin *data_pin_;
GPIOPin *clock_pin_;
uint8_t model_id_;
uint8_t max_power_color_channels_{2};
uint8_t max_power_white_channels_{4};
uint8_t update_channel_;
std::vector<uint16_t> pwm_amounts_;
bool update_{true};
GPIOPin *const data_pin_;
GPIOPin *const clock_pin_;
std::array<uint16_t, 5> pwm_amounts_{};
const uint8_t max_power_color_channels_;
const uint8_t max_power_white_channels_;
};
} // namespace esphome::sm10bit_base
+1 -2
View File
@@ -20,5 +20,4 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
var = await sm10bit_base.register_sm10bit_base(config)
cg.add(var.set_model(0xC0))
await sm10bit_base.register_sm10bit_base(config)
-8
View File
@@ -5,14 +5,6 @@ namespace esphome::sm2235 {
ESPHOME_LOG_TAG(TAG, "sm2235");
void SM2235::setup() {
this->data_pin_->setup();
this->data_pin_->digital_write(true);
this->clock_pin_->setup();
this->clock_pin_->digital_write(true);
this->pwm_amounts_.resize(5, 0);
}
void SM2235::dump_config() {
ESP_LOGCONFIG(TAG,
"SM2235:\n"
+1 -4
View File
@@ -1,16 +1,13 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sm10bit_base/sm10bit_base.h"
#include "esphome/core/hal.h"
namespace esphome::sm2235 {
class SM2235 final : public sm10bit_base::Sm10BitBase {
public:
SM2235() = default;
using Sm10BitBase::Sm10BitBase;
void setup() override;
void dump_config() override;
};
+1 -2
View File
@@ -20,5 +20,4 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config: ConfigType) -> None:
var = await sm10bit_base.register_sm10bit_base(config)
cg.add(var.set_model(0xC0))
await sm10bit_base.register_sm10bit_base(config)
-8
View File
@@ -5,14 +5,6 @@ namespace esphome::sm2335 {
ESPHOME_LOG_TAG(TAG, "sm2335");
void SM2335::setup() {
this->data_pin_->setup();
this->data_pin_->digital_write(true);
this->clock_pin_->setup();
this->clock_pin_->digital_write(true);
this->pwm_amounts_.resize(5, 0);
}
void SM2335::dump_config() {
ESP_LOGCONFIG(TAG,
"sm2335:\n"
+1 -4
View File
@@ -1,16 +1,13 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sm10bit_base/sm10bit_base.h"
#include "esphome/core/hal.h"
namespace esphome::sm2335 {
class SM2335 final : public sm10bit_base::Sm10BitBase {
public:
SM2335() = default;
using Sm10BitBase::Sm10BitBase;
void setup() override;
void dump_config() override;
};
@@ -84,14 +84,10 @@ bool ThermoProBLE::parse_device(const ble_device_base::ESPBTDevice &device) {
}
// publish sensor values
if (result->temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*result->temperature);
if (result->external_temperature.has_value() && this->external_temperature_sensor_ != nullptr)
this->external_temperature_sensor_->publish_state(*result->external_temperature);
if (result->humidity.has_value() && this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(*result->humidity);
if (result->battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*result->battery_level);
publish_optional(this->temperature_sensor_, result->temperature);
publish_optional(this->external_temperature_sensor_, result->external_temperature);
publish_optional(this->humidity_sensor_, result->humidity);
publish_optional(this->battery_level_sensor_, result->battery_level);
success = true;
}
+9 -23
View File
@@ -2,6 +2,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/components/i2c/i2c.h"
#include <cinttypes>
@@ -17,27 +18,12 @@ static const uint8_t SYSCONFIG_ADDRESS = 0xA5;
static const uint8_t PCONFIG_ADDRESS = 0xA6;
static const uint8_t READ_COMMAND = 0x0A;
[[maybe_unused]] static const char *oversampling_to_str(XGZP68XXOversampling oversampling) {
switch (oversampling) {
case XGZP68XX_OVERSAMPLING_256X:
return "256x";
case XGZP68XX_OVERSAMPLING_512X:
return "512x";
case XGZP68XX_OVERSAMPLING_1024X:
return "1024x";
case XGZP68XX_OVERSAMPLING_2048X:
return "2048x";
case XGZP68XX_OVERSAMPLING_4096X:
return "4096x";
case XGZP68XX_OVERSAMPLING_8192X:
return "8192x";
case XGZP68XX_OVERSAMPLING_16384X:
return "16384x";
case XGZP68XX_OVERSAMPLING_32768X:
return "32768x";
default:
return "UNKNOWN";
}
// Indexed by the XGZP68XXOversampling register value, which is not in size order
PROGMEM_STRING_TABLE(OversamplingStrings, "1024x", "2048x", "4096x", "8192x", "256x", "512x", "16384x", "32768x",
"UNKNOWN");
[[maybe_unused]] static const LogString *oversampling_to_str(XGZP68XXOversampling value) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(value), OversamplingStrings::LAST_INDEX);
}
void XGZP68XXComponent::update() {
@@ -48,7 +34,7 @@ void XGZP68XXComponent::update() {
uint8_t newconfig = (oldconfig & 0xf8) | (this->pressure_oversampling_ & 0x7);
this->write_register(PCONFIG_ADDRESS, &newconfig, 1);
ESP_LOGD(TAG, "oversampling to %s: oldconfig = 0x%x newconfig = 0x%x",
oversampling_to_str(this->pressure_oversampling_), oldconfig, newconfig);
LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)), oldconfig, newconfig);
this->last_pressure_oversampling_ = this->pressure_oversampling_;
}
@@ -106,7 +92,7 @@ void XGZP68XXComponent::dump_config() {
LOG_SENSOR(" ", "Temperature: ", this->temperature_sensor_);
LOG_SENSOR(" ", "Pressure: ", this->pressure_sensor_);
if (this->pressure_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)));
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
@@ -15,12 +15,9 @@ void XiaomiCGPR1::dump_config() {
}
void XiaomiCGPR1::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.idle_time.has_value() && this->idle_time_sensor_ != nullptr)
this->idle_time_sensor_->publish_state(*res.idle_time);
if (res.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res.battery_level);
if (res.illuminance.has_value() && this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(*res.illuminance);
publish_optional(this->idle_time_sensor_, res.idle_time);
publish_optional(this->battery_level_sensor_, res.battery_level);
publish_optional(this->illuminance_sensor_, res.illuminance);
if (res.has_motion.has_value())
this->publish_state(*res.has_motion);
}
@@ -14,14 +14,10 @@ void XiaomiGCLS002::dump_config() {
}
void XiaomiGCLS002::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*res.temperature);
if (res.moisture.has_value() && this->moisture_sensor_ != nullptr)
this->moisture_sensor_->publish_state(*res.moisture);
if (res.conductivity.has_value() && this->conductivity_sensor_ != nullptr)
this->conductivity_sensor_->publish_state(*res.conductivity);
if (res.illuminance.has_value() && this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(*res.illuminance);
publish_optional(this->temperature_sensor_, res.temperature);
publish_optional(this->moisture_sensor_, res.moisture);
publish_optional(this->conductivity_sensor_, res.conductivity);
publish_optional(this->illuminance_sensor_, res.illuminance);
}
} // namespace esphome::xiaomi_gcls002
@@ -15,16 +15,11 @@ void XiaomiHHCCJCY01::dump_config() {
}
void XiaomiHHCCJCY01::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*res.temperature);
if (res.moisture.has_value() && this->moisture_sensor_ != nullptr)
this->moisture_sensor_->publish_state(*res.moisture);
if (res.conductivity.has_value() && this->conductivity_sensor_ != nullptr)
this->conductivity_sensor_->publish_state(*res.conductivity);
if (res.illuminance.has_value() && this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(*res.illuminance);
if (res.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res.battery_level);
publish_optional(this->temperature_sensor_, res.temperature);
publish_optional(this->moisture_sensor_, res.moisture);
publish_optional(this->conductivity_sensor_, res.conductivity);
publish_optional(this->illuminance_sensor_, res.illuminance);
publish_optional(this->battery_level_sensor_, res.battery_level);
}
} // namespace esphome::xiaomi_hhccjcy01
@@ -12,10 +12,8 @@ void XiaomiHHCCPOT002::dump_config() {
}
void XiaomiHHCCPOT002::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.moisture.has_value() && this->moisture_sensor_ != nullptr)
this->moisture_sensor_->publish_state(*res.moisture);
if (res.conductivity.has_value() && this->conductivity_sensor_ != nullptr)
this->conductivity_sensor_->publish_state(*res.conductivity);
publish_optional(this->moisture_sensor_, res.moisture);
publish_optional(this->conductivity_sensor_, res.conductivity);
}
} // namespace esphome::xiaomi_hhccpot002
@@ -14,14 +14,10 @@ void XiaomiJQJCY01YM::dump_config() {
}
void XiaomiJQJCY01YM::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.temperature.has_value() && this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(*res.temperature);
if (res.humidity.has_value() && this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(*res.humidity);
if (res.formaldehyde.has_value() && this->formaldehyde_sensor_ != nullptr)
this->formaldehyde_sensor_->publish_state(*res.formaldehyde);
if (res.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res.battery_level);
publish_optional(this->temperature_sensor_, res.temperature);
publish_optional(this->humidity_sensor_, res.humidity);
publish_optional(this->formaldehyde_sensor_, res.formaldehyde);
publish_optional(this->battery_level_sensor_, res.battery_level);
}
} // namespace esphome::xiaomi_jqjcy01ym
@@ -69,10 +69,8 @@ bool XiaomiMCCGQ02HL::parse_device(const ble_device_base::ESPBTDevice &device) {
if (reading.open.has_value())
this->publish_state(*reading.open);
if (reading.light.has_value() && this->light_binary_sensor_ != nullptr)
this->light_binary_sensor_->publish_state(*reading.light);
if (reading.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*reading.battery_level);
publish_optional(this->light_binary_sensor_, reading.light);
publish_optional(this->battery_level_sensor_, reading.battery_level);
success = true;
}
return success;
@@ -34,8 +34,7 @@ bool XiaomiMiscale::parse_device(const ble_device_base::ESPBTDevice &device) {
if (!report_results_(res, addr_str))
continue;
if (res->weight.has_value() && this->weight_sensor_ != nullptr)
this->weight_sensor_->publish_state(*res->weight);
publish_optional(this->weight_sensor_, res->weight);
if (this->impedance_sensor_ != nullptr) {
if (res->version == 1) {
@@ -16,14 +16,10 @@ void XiaomiMJYD02YLA::dump_config() {
}
void XiaomiMJYD02YLA::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.idle_time.has_value() && this->idle_time_sensor_ != nullptr)
this->idle_time_sensor_->publish_state(*res.idle_time);
if (res.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res.battery_level);
if (res.illuminance.has_value() && this->illuminance_sensor_ != nullptr)
this->illuminance_sensor_->publish_state(*res.illuminance);
if (res.is_light.has_value() && this->light_binary_sensor_ != nullptr)
this->light_binary_sensor_->publish_state(*res.is_light);
publish_optional(this->idle_time_sensor_, res.idle_time);
publish_optional(this->battery_level_sensor_, res.battery_level);
publish_optional(this->illuminance_sensor_, res.illuminance);
publish_optional(this->light_binary_sensor_, res.is_light);
if (res.has_motion.has_value())
this->publish_state(*res.has_motion);
}
@@ -28,8 +28,7 @@ void XiaomiRTCGQ02LM::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
this->set_timeout(MOTION_TIMEOUT_ID, this->motion_timeout_,
[this]() { this->motion_binary_sensor_->publish_state(false); });
}
if (res.is_light.has_value() && this->light_binary_sensor_ != nullptr)
this->light_binary_sensor_->publish_state(*res.is_light);
publish_optional(this->light_binary_sensor_, res.is_light);
if (res.button_press.has_value() && this->button_binary_sensor_ != nullptr) {
this->button_binary_sensor_->publish_state(*res.button_press);
this->set_timeout(BUTTON_TIMEOUT_ID, this->button_timeout_,
@@ -37,8 +36,7 @@ void XiaomiRTCGQ02LM::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
}
#endif
#ifdef USE_SENSOR
if (res.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res.battery_level);
publish_optional(this->battery_level_sensor_, res.battery_level);
#endif
}
@@ -16,10 +16,8 @@ void XiaomiWX08ZM::publish_result(const xiaomi_ble::XiaomiParseResult &res) {
if (res.is_active.has_value()) {
this->publish_state(*res.is_active);
}
if (res.tablet.has_value() && this->tablet_sensor_ != nullptr)
this->tablet_sensor_->publish_state(*res.tablet);
if (res.battery_level.has_value() && this->battery_level_sensor_ != nullptr)
this->battery_level_sensor_->publish_state(*res.battery_level);
publish_optional(this->tablet_sensor_, res.tablet);
publish_optional(this->battery_level_sensor_, res.battery_level);
}
} // namespace esphome::xiaomi_wx08zm
+1
View File
@@ -0,0 +1 @@
"""Tests for the LD6004 radar component."""
@@ -0,0 +1,20 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
uart:
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 115200
ld6004:
id: radar
binary_sensor:
- platform: ld6004
target:
name: Presence
@@ -0,0 +1,2 @@
packages:
base: !include base.yaml
@@ -0,0 +1,8 @@
packages:
base: !include base.yaml
sensor:
- platform: ld6004
detection_area_0:
x_min:
name: Detection X Min
@@ -0,0 +1,8 @@
packages:
base: !include base.yaml
sensor:
- platform: ld6004
dwell_area_1:
x_min:
name: Dwell X Min
@@ -0,0 +1,28 @@
"""The LD6004 sizes its dwell areas only when an entity uses them."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.core import CORE
@pytest.mark.parametrize(
("config_file", "area_kinds"),
[
("binary_sensor_only.yaml", "2"),
("detection_area_sensor.yaml", "2"),
("dwell_area_sensor.yaml", "3"),
],
)
def test_area_kinds(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
area_kinds: str,
) -> None:
generate_main(component_config_path(config_file))
defines = {define.name: str(define.value) for define in CORE.defines}
assert defines["LD600X_AREA_KINDS"] == area_kinds
assert defines["LD600X_MAX_TARGETS"] == "3"
+42
View File
@@ -0,0 +1,42 @@
"""Validate the LD6004 hub and binary sensor configuration limits."""
import pytest
from esphome.components.ld6004 import CONFIG_SCHEMA, LD6004Component
from esphome.components.ld6004.binary_sensor import (
CONFIG_SCHEMA as BINARY_SENSOR_SCHEMA,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID, PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def test_minimal_hub(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
config: ConfigType = CONFIG_SCHEMA({})
assert config[CONF_ID].type == LD6004Component
@pytest.mark.parametrize(
("key", "value"),
[("out_pin", 4), ("stale_timeout", "1s"), ("wakeup_pin", 4)],
)
def test_unsupported_hub_option(
set_core_config: SetCoreConfigCallable, key: str, value: int | str
) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
with pytest.raises(cv.Invalid, match=key):
CONFIG_SCHEMA({key: value})
def test_target_3(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
config: ConfigType = BINARY_SENSOR_SCHEMA({"target_3": {"name": "Target 3"}})
assert config["target_3"]["name"] == "Target 3"
def test_target_4_rejected(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
with pytest.raises(cv.Invalid, match="target_4"):
BINARY_SENSOR_SCHEMA({"target_4": {"name": "Target 4"}})
@@ -0,0 +1,48 @@
"""Validate LD6004 sensor target and area limits."""
import pytest
from esphome.components.ld6004 import LD6004Component
from esphome.components.ld6004.sensor import CONFIG_SCHEMA
import esphome.config_validation as cv
from esphome.const import PlatformFramework
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
@pytest.mark.parametrize(
("key", "fields"),
[
("target_3", ("x", "y", "z", "doppler_index", "cluster_id")),
("dwell_area_3", ("x_min", "x_max", "y_min", "y_max", "z_min", "z_max")),
],
)
def test_model_sensor_keys(
set_core_config: SetCoreConfigCallable, key: str, fields: tuple[str, ...]
) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
config: ConfigType = CONFIG_SCHEMA(
{key: {field: {"name": field} for field in fields}}
)
assert config["ld6004_id"].type == LD6004Component
assert set(config[key]) == set(fields)
for field in fields:
assert config[key][field]["name"] == field
@pytest.mark.parametrize(
"key",
[
"target_0",
"target_4",
"interference_area_4",
"detection_area_4",
"dwell_area_4",
],
)
def test_out_of_range_sensor_keys(
set_core_config: SetCoreConfigCallable, key: str
) -> None:
set_core_config(PlatformFramework.ESP32_IDF)
with pytest.raises(cv.Invalid, match=key):
CONFIG_SCHEMA({key: {"x": {"name": "Invalid sensor"}}})
@@ -23,7 +23,7 @@ def test_requests_keep_the_largest_count() -> None:
assert defines["LD600X_MAX_TARGETS"] == "3"
def test_two_area_kinds_by_default() -> None:
def test_one_request_sets_its_area_kind_count() -> None:
request_model_sizes(MockObj("hub"), area_kinds=2)
CORE.flush_tasks()
assert _defines()["LD600X_AREA_KINDS"] == "2"
+5 -4
View File
@@ -72,6 +72,7 @@ TEST_F(GCJA5Test, BadChecksumIsDropped) {
this->feed_(frame(11, 21));
ASSERT_EQ(this->pm_2_5_published_.size(), 1u);
EXPECT_FLOAT_EQ(this->pm_2_5_published_[0], 11);
EXPECT_FALSE(this->sut_.status_has_warning());
}
TEST_F(GCJA5Test, BadEndByteIsDropped) {
@@ -81,14 +82,14 @@ TEST_F(GCJA5Test, BadEndByteIsDropped) {
EXPECT_TRUE(this->pm_2_5_published_.empty());
}
// Pins current behaviour: once one good frame was seen, the values of a later bad frame are published
TEST_F(GCJA5Test, BadFrameAfterGoodFrameIsPublished) {
TEST_F(GCJA5Test, BadFrameAfterGoodFrameIsNotPublished) {
this->feed_(frame(10, 20));
auto bad = frame(99, 98);
bad[30] ^= 1;
this->feed_(bad);
ASSERT_EQ(this->pm_2_5_published_.size(), 2u);
EXPECT_FLOAT_EQ(this->pm_2_5_published_[1], 99);
ASSERT_EQ(this->pm_2_5_published_.size(), 1u);
EXPECT_FLOAT_EQ(this->pm_2_5_published_[0], 10);
EXPECT_TRUE(this->sut_.status_has_warning());
}
TEST_F(GCJA5Test, SplitReadsAreJoined) {
+2 -2
View File
@@ -148,8 +148,8 @@ inline void apply_select(select::Select *entity, size_t index) {
test_hub()->set_select_value(entity, index);
}
inline void apply_switch(switch_::Switch *entity, bool state) {
entity->publish_state(state);
test_hub()->set_switch_state(entity, state);
if (test_hub()->set_switch_state(entity, state))
entity->publish_state(state);
}
using TestNumber = number::ApplyNumber<apply_number>;
@@ -16,6 +16,13 @@ TEST_F(LD6002BTest, EmptyLowPowerReportIsIgnored) {
EXPECT_TRUE(this->low_power_.state);
}
TEST_F(LD6002BTest, LowPowerSwitchKeepsStateWhenQueueFull) {
for (int i = 0; i < 32; i++)
this->sensitivity_.make_call().set_index(0).perform();
this->low_power_.turn_on();
EXPECT_FALSE(this->low_power_.state);
}
} // namespace esphome::ld6002b::testing
#endif // USE_HOST
@@ -0,0 +1,96 @@
#ifdef USE_HOST
#include "../common.h"
namespace esphome::ld6004::testing {
TEST_F(LD6004Test, TargetReportSetsPresence) {
this->feed_(target_report(1));
EXPECT_TRUE(this->presence_.state);
EXPECT_TRUE(this->target_presence_[0].state);
EXPECT_FALSE(this->target_presence_[1].state);
this->feed_(target_report(0));
EXPECT_FALSE(this->presence_.state);
EXPECT_FALSE(this->target_presence_[0].state);
}
TEST_F(LD6004Test, AreaPresenceReportSetsAreaAndPresence) {
this->feed_(area_presence({0, 0, 1, 0}));
EXPECT_FALSE(this->area_presence_[0].state);
EXPECT_TRUE(this->area_presence_[2].state);
EXPECT_TRUE(this->presence_.state);
}
TEST_F(LD6004Test, IdleWorkStatusClearsAreaPresence) {
this->feed_(area_presence({0, 1, 0, 0}));
this->feed_(tf_frame(TYPE_REPORT_WORK_STATUS, {0x00}));
EXPECT_FALSE(this->area_presence_[1].state);
EXPECT_FALSE(this->presence_.state);
}
TEST_F(LD6004Test, IdleWorkStatusKeepsAreaPresenceWhileTracking) {
this->feed_(target_report(1));
this->feed_(area_presence({0, 1, 0, 0}));
this->feed_(tf_frame(TYPE_REPORT_WORK_STATUS, {0x00}));
EXPECT_TRUE(this->area_presence_[1].state);
EXPECT_TRUE(this->presence_.state);
}
TEST_F(LD6004Test, BusyWorkStatusKeepsAreaPresence) {
this->feed_(area_presence({0, 1, 0, 0}));
this->feed_(tf_frame(TYPE_REPORT_WORK_STATUS, {0x01}));
EXPECT_TRUE(this->area_presence_[1].state);
}
class LD6004WorkModeTest : public LD6004Test, public ::testing::WithParamInterface<uint8_t> {};
// Only the two radar-off modes stop detection; the module sends the mode as one byte or as a uint32
TEST_P(LD6004WorkModeTest, RadarOffModesClearPresence) {
const uint8_t mode = GetParam();
const bool radar_off = mode == WORK_MODE_RADAR_OFF_P20_HIGH || mode == WORK_MODE_RADAR_OFF_P20_LOW;
for (bool wide : {false, true}) {
this->feed_(work_mode(WORK_MODE_NORMAL));
this->feed_(target_report(2));
this->feed_(area_presence({1, 0, 0, 1}));
this->feed_(wide ? tf_frame(TYPE_REPORT_WORK_MODE, u32_payload(mode)) : work_mode(mode));
EXPECT_EQ(this->presence_.state, !radar_off);
EXPECT_EQ(this->target_presence_[0].state, !radar_off);
EXPECT_EQ(this->target_presence_[1].state, !radar_off);
EXPECT_EQ(this->area_presence_[0].state, !radar_off);
EXPECT_EQ(this->area_presence_[3].state, !radar_off);
// Cleared to off, not to unknown
EXPECT_TRUE(this->presence_.has_state());
EXPECT_TRUE(this->target_presence_[0].has_state());
EXPECT_TRUE(this->area_presence_[0].has_state());
}
}
INSTANTIATE_TEST_SUITE_P(Modes, LD6004WorkModeTest,
::testing::Values(WORK_MODE_NORMAL, WORK_MODE_LOW_POWER, WORK_MODE_RADAR_OFF_P20_HIGH,
WORK_MODE_RADAR_OFF_P20_LOW, WORK_MODE_HIGH_REFLECTIVITY));
TEST_F(LD6004Test, TrailingReportsDoNotRestorePresenceWhileRadarIsOff) {
this->feed_(target_report(1));
this->feed_(area_presence({0, 1, 0, 0}));
this->feed_(work_mode(WORK_MODE_RADAR_OFF_P20_LOW));
this->feed_(target_report(1));
this->feed_(area_presence({0, 1, 0, 0}));
EXPECT_FALSE(this->presence_.state);
EXPECT_FALSE(this->target_presence_[0].state);
EXPECT_FALSE(this->area_presence_[1].state);
}
TEST_F(LD6004Test, ReportsCountAgainOnceRadarIsBackOn) {
this->feed_(work_mode(WORK_MODE_RADAR_OFF_P20_HIGH));
this->feed_(work_mode(WORK_MODE_HIGH_REFLECTIVITY));
this->feed_(target_report(1));
EXPECT_TRUE(this->target_presence_[0].state);
this->feed_(target_report(0));
this->feed_(area_presence({0, 0, 1, 0}));
EXPECT_TRUE(this->area_presence_[2].state);
EXPECT_TRUE(this->presence_.state);
}
} // namespace esphome::ld6004::testing
#endif // USE_HOST
+172
View File
@@ -0,0 +1,172 @@
#pragma once
#ifdef USE_HOST
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <thread>
#include <vector>
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/host/preferences.h"
#include "esphome/components/ld6004/ld6004.h"
#include "esphome/components/ld600x/ld600x_frame.h"
#include "esphome/components/uart/uart_component.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::ld6004::testing {
using ld600x::AREA_COUNT;
static constexpr uint8_t MAX_TARGETS = LD600X_MAX_TARGETS;
static constexpr uint16_t TYPE_CONTROL = 0x0201;
static constexpr uint16_t TYPE_REPORT_TARGET = 0x0A04;
static constexpr uint16_t TYPE_REPORT_AREA_PRESENCE = 0x0A0A;
static constexpr uint16_t TYPE_REPORT_WORK_MODE = 0x0A12;
static constexpr uint16_t TYPE_REPORT_WORK_STATUS = 0x0A14;
static constexpr uint32_t CMD_POINT_CLOUD_OFF = 0x07;
static constexpr uint32_t CMD_TARGET_DISPLAY_ON = 0x08;
// Every frame this component sends has the top bit of its id set.
static constexpr uint16_t FIRST_COMMAND_ID = 0x8000;
// The 0x0A12 values, in the order of the work_mode select options.
enum WorkMode : uint8_t {
WORK_MODE_NORMAL,
WORK_MODE_LOW_POWER,
WORK_MODE_RADAR_OFF_P20_HIGH,
WORK_MODE_RADAR_OFF_P20_LOW,
WORK_MODE_HIGH_REFLECTIVITY,
WORK_MODE_COUNT,
};
class MockUARTComponent : public uart::UARTComponent {
public:
std::vector<uint8_t> rx;
std::vector<uint8_t> tx;
void write_array(const uint8_t *data, size_t len) override { this->tx.insert(this->tx.end(), data, data + len); }
bool read_array(uint8_t *data, size_t len) override {
if (this->rx.size() < len)
return false;
std::copy(this->rx.begin(), this->rx.begin() + len, data);
this->rx.erase(this->rx.begin(), this->rx.begin() + len);
return true;
}
size_t available() override { return this->rx.size(); }
MOCK_METHOD(bool, peek_byte, (uint8_t *data), (override));
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override));
#if defined(USE_ESP8266) || defined(USE_ESP32)
void load_settings(bool dump_config) override {}
#endif
};
inline void put_u32(std::vector<uint8_t> &out, uint32_t value) {
for (int i = 0; i < 4; i++)
out.push_back(static_cast<uint8_t>(value >> (8 * i)));
}
inline void put_f32(std::vector<uint8_t> &out, float value) { put_u32(out, bit_cast<uint32_t>(value)); }
inline std::vector<uint8_t> tf_frame(uint16_t type, const std::vector<uint8_t> &payload, uint16_t id = 0x0001) {
std::vector<uint8_t> out(ld600x::TF_FRAME_OVERHEAD + payload.size());
out.resize(ld600x::encode_frame(id, type, payload.empty() ? nullptr : payload.data(),
static_cast<uint16_t>(payload.size()), out.data()));
return out;
}
// An ACK is the command's own type with an empty payload.
inline std::vector<uint8_t> ack(uint16_t type) { return tf_frame(type, {}); }
inline std::vector<uint8_t> u32_payload(uint32_t value) {
std::vector<uint8_t> p;
put_u32(p, value);
return p;
}
// What the component is expected to put on the wire for a command
inline std::vector<uint8_t> command(uint16_t id, uint16_t type, const std::vector<uint8_t> &payload) {
return tf_frame(type, payload, id);
}
inline std::vector<uint8_t> work_mode(uint8_t mode) { return tf_frame(TYPE_REPORT_WORK_MODE, {mode}); }
// One target at (1, 1, 1) with the given cluster id, or an empty report when count is 0.
inline std::vector<uint8_t> target_report(uint8_t count) {
std::vector<uint8_t> p;
put_u32(p, count);
for (uint8_t i = 0; i < count; i++) {
put_f32(p, 1.0f);
put_f32(p, 1.0f);
put_f32(p, 1.0f);
put_u32(p, 0);
put_u32(p, 10 + i);
}
return tf_frame(TYPE_REPORT_TARGET, p);
}
inline std::vector<uint8_t> area_presence(const std::array<uint32_t, AREA_COUNT> &states) {
std::vector<uint8_t> p;
for (uint32_t s : states)
put_u32(p, s);
return tf_frame(TYPE_REPORT_AREA_PRESENCE, p);
}
/// A hub with the presence binary sensors, set up as codegen does and past its boot timeout.
class LD6004Test : public ::testing::Test {
protected:
void SetUp() override {
this->temp_dir_ = std::filesystem::temp_directory_path() / "esphome_ld6004_test";
std::filesystem::remove_all(this->temp_dir_);
std::filesystem::create_directories(this->temp_dir_);
setenv("ESPHOME_PREFDIR", this->temp_dir_.c_str(), 1);
host::host_preferences = &this->prefs_;
global_preferences = &this->prefs_;
this->sut_.set_uart_parent(&this->uart_);
this->sut_.set_presence_binary_sensor(&this->presence_);
for (uint8_t i = 0; i < MAX_TARGETS; i++)
this->sut_.set_target_presence_binary_sensor(i, &this->target_presence_[i]);
for (uint8_t i = 0; i < AREA_COUNT; i++)
this->sut_.set_area_presence_binary_sensor(i, &this->area_presence_[i]);
this->sut_.setup();
// Past setup()'s 100 ms boot timeout, which starts the streams and queues the queries
std::this_thread::sleep_for(std::chrono::milliseconds(120));
App.scheduler.call(millis());
}
void TearDown() override {
host::setup_preferences();
unsetenv("ESPHOME_PREFDIR");
std::filesystem::remove_all(this->temp_dir_);
}
void feed_(const std::vector<uint8_t> &bytes) {
this->uart_.rx.insert(this->uart_.rx.end(), bytes.begin(), bytes.end());
this->sut_.loop();
}
host::HostPreferences prefs_;
std::filesystem::path temp_dir_;
MockUARTComponent uart_;
LD6004Component sut_;
binary_sensor::BinarySensor presence_;
binary_sensor::BinarySensor target_presence_[MAX_TARGETS];
binary_sensor::BinarySensor area_presence_[AREA_COUNT];
};
} // namespace esphome::ld6004::testing
#endif // USE_HOST
+83
View File
@@ -0,0 +1,83 @@
ld6004:
id: ld6004_radar
binary_sensor:
- platform: ld6004
ld6004_id: ld6004_radar
target:
name: LD6004 Presence
target_1:
name: LD6004 Target 1
target_3:
name: LD6004 Target 3
detection_area_0:
name: LD6004 Detection Area 0
sensor:
- platform: ld6004
ld6004_id: ld6004_radar
target_count:
name: LD6004 Target Count
point_count:
name: LD6004 Point Count
target_1:
x:
name: LD6004 Target-1 X
y:
name: LD6004 Target-1 Y
z:
name: LD6004 Target-1 Z
doppler_index:
name: LD6004 Target-1 Dop
cluster_id:
name: LD6004 Target-1 Cluster
target_3:
x:
name: LD6004 Target-3 X
y:
name: LD6004 Target-3 Y
z:
name: LD6004 Target-3 Z
doppler_index:
name: LD6004 Target-3 Dop
cluster_id:
name: LD6004 Target-3 Cluster
interference_area_0:
x_min:
name: LD6004 Interference-0 X Min
x_max:
name: LD6004 Interference-0 X Max
y_min:
name: LD6004 Interference-0 Y Min
y_max:
name: LD6004 Interference-0 Y Max
z_min:
name: LD6004 Interference-0 Z Min
z_max:
name: LD6004 Interference-0 Z Max
detection_area_0:
x_min:
name: LD6004 Detection-0 X Min
x_max:
name: LD6004 Detection-0 X Max
y_min:
name: LD6004 Detection-0 Y Min
y_max:
name: LD6004 Detection-0 Y Max
z_min:
name: LD6004 Detection-0 Z Min
z_max:
name: LD6004 Detection-0 Z Max
dwell_area_3:
x_min:
name: LD6004 Dwell-3 X Min
x_max:
name: LD6004 Dwell-3 X Max
y_min:
name: LD6004 Dwell-3 Y Min
y_max:
name: LD6004 Dwell-3 Y Max
z_min:
name: LD6004 Dwell-3 Z Min
z_max:
name: LD6004 Dwell-3 Z Max
@@ -0,0 +1,47 @@
#ifdef USE_HOST
#include "common.h"
namespace esphome::ld6004::testing {
TEST_F(LD6004Test, BootStartsTargetStreamFirst) {
EXPECT_EQ(this->uart_.tx, command(FIRST_COMMAND_ID, TYPE_CONTROL, u32_payload(CMD_TARGET_DISPLAY_ON)));
}
// With only presence configured, boot stops the point cloud and sends no model query
TEST_F(LD6004Test, BootStopsPointCloudThenGoesQuiet) {
this->uart_.tx.clear();
this->feed_(ack(TYPE_CONTROL));
EXPECT_EQ(this->uart_.tx, command(FIRST_COMMAND_ID + 1, TYPE_CONTROL, u32_payload(CMD_POINT_CLOUD_OFF)));
this->uart_.tx.clear();
this->feed_(ack(TYPE_CONTROL));
EXPECT_TRUE(this->uart_.tx.empty());
}
TEST_F(LD6004Test, ReportsSendNothing) {
this->uart_.tx.clear();
this->feed_(work_mode(WORK_MODE_RADAR_OFF_P20_HIGH));
this->feed_(tf_frame(TYPE_REPORT_WORK_STATUS, {0x00}));
this->feed_(target_report(1));
EXPECT_TRUE(this->uart_.tx.empty());
}
// A malformed model report is claimed and dropped, and the next frame still parses
TEST_F(LD6004Test, MalformedWorkModeReportIsIgnored) {
this->feed_(target_report(1));
this->feed_(work_mode(WORK_MODE_COUNT));
this->feed_(tf_frame(TYPE_REPORT_WORK_MODE, {WORK_MODE_RADAR_OFF_P20_HIGH, 0x00}));
this->feed_(tf_frame(TYPE_REPORT_WORK_MODE, {}));
EXPECT_TRUE(this->presence_.state);
this->feed_(target_report(0));
EXPECT_FALSE(this->presence_.state);
}
TEST_F(LD6004Test, EmptyWorkStatusReportIsIgnored) {
this->feed_(area_presence({1, 0, 0, 0}));
this->feed_(tf_frame(TYPE_REPORT_WORK_STATUS, {}));
EXPECT_TRUE(this->area_presence_[0].state);
}
} // namespace esphome::ld6004::testing
#endif // USE_HOST
@@ -0,0 +1,41 @@
#ifdef USE_HOST
#include "../common.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::ld6004::testing {
namespace {
constexpr uint16_t TYPE_REPORT_DWELL_AREAS = 0x0A16;
} // namespace
// A 0x0A16 report lands on the dwell areas only, not on the interference or detection ones
TEST_F(LD6004Test, DwellAreaReportPublishesDwellBounds) {
sensor::Sensor dwell[ld600x::AREA_AXIS_COUNT];
sensor::Sensor interference_x_min;
sensor::Sensor detection_x_min;
for (uint8_t axis = 0; axis < ld600x::AREA_AXIS_COUNT; axis++)
this->sut_.set_area_sensor(ld600x::AREA_KIND_DWELL, 3, axis, &dwell[axis]);
this->sut_.set_area_sensor(ld600x::AREA_KIND_INTERFERENCE, 3, ld600x::AREA_AXIS_X_MIN, &interference_x_min);
this->sut_.set_area_sensor(ld600x::AREA_KIND_DETECTION, 3, ld600x::AREA_AXIS_X_MIN, &detection_x_min);
// x_min, x_max, y_min, y_max, z_min, z_max for area 3; the other areas are zero
const float bounds[ld600x::AREA_AXIS_COUNT] = {-1.5f, 1.25f, 0.5f, 3.75f, -0.25f, 2.5f};
std::vector<uint8_t> p;
for (uint8_t i = 0; i < AREA_COUNT; i++) {
for (float v : bounds)
put_f32(p, i == 3 ? v : 0.0f);
}
this->feed_(tf_frame(TYPE_REPORT_DWELL_AREAS, p));
for (uint8_t axis = 0; axis < ld600x::AREA_AXIS_COUNT; axis++)
EXPECT_FLOAT_EQ(dwell[axis].state, bounds[axis]) << "axis " << int(axis);
EXPECT_FALSE(interference_x_min.has_state());
EXPECT_FALSE(detection_x_min.has_state());
}
} // namespace esphome::ld6004::testing
#endif // USE_HOST
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
ld6004: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
ld6004: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
ld6004: !include common.yaml
@@ -0,0 +1,160 @@
esphome:
name: test-binary-delay-filters
host:
api:
logger:
level: DEBUG
binary_sensor:
- platform: template
name: "Source"
id: source
trigger_on_initial_state: true
on_state:
- logger.log:
format: "SRC %s @%u"
args: ['x ? "ON" : "OFF"', 'millis()']
- platform: template
id: x
- platform: copy
source_id: source
id: on_off_single
internal: true
trigger_on_initial_state: true
filters:
- delayed_on_off: 100ms
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"on_off_single"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: on_off_split
internal: true
trigger_on_initial_state: true
filters:
- delayed_on_off:
time_on: 100ms
time_off: 300ms
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"on_off_split"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: on_off_lambda
internal: true
trigger_on_initial_state: true
filters:
- delayed_on_off:
time_on: !lambda return 100;
time_off: !lambda |-
uint32_t delay = 300;
return delay;
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"on_off_lambda"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: on_off_id_x
internal: true
trigger_on_initial_state: true
filters:
- delayed_on_off: !lambda 'return id(x).state ? 100 : 300;'
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"on_off_id_x"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: delayed_on
internal: true
trigger_on_initial_state: true
filters:
- delayed_on: 100ms
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"delayed_on"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: delayed_on_lambda
internal: true
trigger_on_initial_state: true
filters:
- delayed_on: !lambda return 100;
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"delayed_on_lambda"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: delayed_off
internal: true
trigger_on_initial_state: true
filters:
- delayed_off: 100ms
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"delayed_off"', 'x ? "ON" : "OFF"', 'millis()']
- platform: copy
source_id: source
id: lambda_filter
internal: true
trigger_on_initial_state: true
filters:
- lambda: |-
static int count = 0;
if (++count == 2)
return {};
return !x;
on_state:
- logger.log:
format: "OUT %s=%s @%u"
args: ['"lambda_filter"', 'x ? "ON" : "OFF"', 'millis()']
button:
- platform: template
name: "Run Filters"
id: run_filters
on_press:
- binary_sensor.template.publish:
id: x
state: ON
- binary_sensor.template.publish:
id: source
state: ON
- delay: 30ms
- binary_sensor.template.publish:
id: source
state: OFF
- delay: 30ms
- binary_sensor.template.publish:
id: source
state: ON
- delay: 500ms
- binary_sensor.template.publish:
id: source
state: OFF
- delay: 150ms
- binary_sensor.template.publish:
id: source
state: ON
- delay: 500ms
- binary_sensor.template.publish:
id: source
state: OFF
- delay: 500ms
- logger.log: "FILTERS DONE"
@@ -0,0 +1,274 @@
esphome:
name: test-sensor-filters-apply
host:
api:
logger:
level: DEBUG
sensor:
- platform: template
name: "Source"
id: source
update_interval: never
- platform: template
id: x
update_interval: never
- platform: copy
source_id: source
id: offset_const
internal: true
filters:
- offset: 2.5
on_value:
- logger.log:
format: "OUT offset_const=%.9g"
args: [x]
- platform: copy
source_id: source
id: offset_lambda
internal: true
filters:
- offset: !lambda |-
float base = 1.0f;
return base + 0.25f;
on_value:
- logger.log:
format: "OUT offset_lambda=%.9g"
args: [x]
- platform: copy
source_id: source
id: offset_id_x
internal: true
filters:
- offset: !lambda return id(x).state;
on_value:
- logger.log:
format: "OUT offset_id_x=%.9g"
args: [x]
- platform: copy
source_id: source
id: multiply_const
internal: true
filters:
- multiply: 1.5
on_value:
- logger.log:
format: "OUT multiply_const=%.9g"
args: [x]
- platform: copy
source_id: source
id: multiply_lambda
internal: true
filters:
- multiply: !lambda return 1.0f + 1.0f;
on_value:
- logger.log:
format: "OUT multiply_lambda=%.9g"
args: [x]
- platform: copy
source_id: source
id: clamp_min
internal: true
filters:
- clamp:
min_value: 0
on_value:
- logger.log:
format: "OUT clamp_min=%.9g"
args: [x]
- platform: copy
source_id: source
id: clamp_max
internal: true
filters:
- clamp:
max_value: 10
on_value:
- logger.log:
format: "OUT clamp_max=%.9g"
args: [x]
- platform: copy
source_id: source
id: clamp_both
internal: true
filters:
- clamp:
min_value: 0
max_value: 10
on_value:
- logger.log:
format: "OUT clamp_both=%.9g"
args: [x]
- platform: copy
source_id: source
id: clamp_ignore
internal: true
filters:
- clamp:
min_value: 0
max_value: 10
ignore_out_of_range: true
on_value:
- logger.log:
format: "OUT clamp_ignore=%.9g"
args: [x]
- platform: copy
source_id: source
id: ntc_resistance
internal: true
filters:
- to_ntc_resistance:
calibration:
b_constant: 3950
reference_temperature: 25°C
reference_resistance: 10kOhm
on_value:
- logger.log:
format: "OUT ntc_resistance=%.9g"
args: [x]
- platform: copy
source_id: source
id: ntc_resistance_abc
internal: true
filters:
- to_ntc_resistance:
calibration:
a: 0.0011
b: 0.00023
c: 0.0000001
on_value:
- logger.log:
format: "OUT ntc_resistance_abc=%.9g"
args: [x]
- platform: copy
source_id: source
id: ntc_temperature
internal: true
filters:
- multiply: 1000
- to_ntc_temperature:
calibration:
b_constant: 3950
reference_temperature: 25°C
reference_resistance: 10kOhm
on_value:
- logger.log:
format: "OUT ntc_temperature=%.9g"
args: [x]
- platform: copy
source_id: source
id: ntc_temperature_abc
internal: true
filters:
- multiply: 1000
- to_ntc_temperature:
calibration:
a: 0.0011
b: 0.00023
c: 0.0000001
on_value:
- logger.log:
format: "OUT ntc_temperature_abc=%.9g"
args: [x]
- platform: copy
source_id: source
id: calibrate_least_squares
internal: true
filters:
- calibrate_linear:
- 0.0 -> 0.0
- 10.0 -> 10.5
- 20.0 -> 19.0
on_value:
- logger.log:
format: "OUT calibrate_least_squares=%.9g"
args: [x]
- platform: copy
source_id: source
id: calibrate_exact
internal: true
filters:
- calibrate_linear:
method: exact
datapoints:
- 0.0 -> 0.0
- 10.0 -> 100.0
- 20.0 -> 150.0
on_value:
- logger.log:
format: "OUT calibrate_exact=%.9g"
args: [x]
- platform: copy
source_id: source
id: lambda_filter
internal: true
filters:
- lambda: |-
if (x > 100.0f)
return {};
return x * 2.0f;
on_value:
- logger.log:
format: "OUT lambda_filter=%.9g"
args: [x]
text_sensor:
- platform: template
id: text_source
update_interval: never
- platform: copy
source_id: text_source
id: text_lambda_replace
internal: true
filters:
- lambda: return {"[" + x + "]"};
on_value:
- logger.log:
format: "TXT text_lambda_replace=%s"
args: [x.c_str()]
- platform: copy
source_id: text_source
id: text_lambda_drop
internal: true
filters:
- lambda: |-
if (x == "skip")
return {};
return x;
on_value:
- logger.log:
format: "TXT text_lambda_drop=%s"
args: [x.c_str()]
button:
- platform: template
name: "Run Filters"
id: run_filters
on_press:
- lambda: |-
id(x).publish_state(0.75f);
for (float value : {-10.0f, 0.0f, 0.5f, 2.25f, 12.345f, 50.0f, 150.0f, NAN, INFINITY})
id(source).publish_state(value);
for (const char *value : {"a", "skip", "b"})
id(text_source).publish_state(value);
ESP_LOGI("test", "FILTERS DONE");
@@ -0,0 +1,85 @@
"""Pin the output of the binary sensor delay and lambda filters for one input sequence.
The source goes ON 0 ms, OFF 30 ms, ON 60 ms, OFF 560 ms, ON 710 ms, OFF 1210 ms.
"""
from __future__ import annotations
import re
import pytest
from .log_utils import LineWaiter
from .state_utils import require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
OUT_PATTERN = re.compile(r"OUT (\w+)=(ON|OFF) @(\d+)")
SRC_PATTERN = re.compile(r"SRC (ON|OFF) @(\d+)")
# How late an output may land after its due time, for scheduler and CI jitter; well below
# the 100 ms shortest delay, so a skipped delay fails
LATE_MS = 60
EARLY_MS = 2
EXPECTED: dict[str, list[str]] = {
# Short pulses are swallowed; each edge lands 100 ms late
"on_off_single": ["ON", "OFF", "ON", "OFF"],
# The 300 ms off delay outlasts the 150 ms OFF at 560 ms, so only the last OFF shows
"on_off_split": ["ON", "OFF"],
"on_off_lambda": ["ON", "OFF"],
# The delay lambda reads the binary sensor with id `x` (ON, so 100 ms), not the filter input
"on_off_id_x": ["ON", "OFF", "ON", "OFF"],
"delayed_on": ["OFF", "ON", "OFF", "ON", "OFF"],
"delayed_on_lambda": ["OFF", "ON", "OFF", "ON", "OFF"],
"delayed_off": ["ON", "OFF", "ON", "OFF"],
# The lambda inverts and drops its second value; repeats do not reach the sensor
"lambda_filter": ["OFF", "ON", "OFF", "ON"],
}
# For each output: the source edge (0 to 5) it follows and the delay in ms after that edge
TIMING: dict[str, list[tuple[int, int]]] = {
"on_off_single": [(2, 100), (3, 100), (4, 100), (5, 100)],
"on_off_split": [(2, 100), (5, 300)],
"on_off_lambda": [(2, 100), (5, 300)],
"on_off_id_x": [(2, 100), (3, 100), (4, 100), (5, 100)],
"delayed_on": [(1, 0), (2, 100), (3, 0), (4, 100), (5, 0)],
"delayed_on_lambda": [(1, 0), (2, 100), (3, 0), (4, 100), (5, 0)],
"delayed_off": [(0, 0), (3, 100), (4, 0), (5, 100)],
"lambda_filter": [(0, 0), (3, 0), (4, 0), (5, 0)],
}
@pytest.mark.asyncio
async def test_binary_sensor_delay_filters(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Each copy sensor runs one filter over the same timed inputs; every output is logged in order."""
waiter = LineWaiter()
async with (
run_compiled(yaml_config, line_callback=waiter.callback),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
done = waiter.expect("FILTERS DONE")
client.button_command(require_entity(entities, "run_filters").key)
await waiter.wait(done, timeout=5.0)
edges: list[int] = []
outputs: dict[str, list[str]] = {name: [] for name in EXPECTED}
times: dict[str, list[int]] = {name: [] for name in EXPECTED}
for line in waiter.lines:
if (match := OUT_PATTERN.search(line)) is not None:
outputs[match.group(1)].append(match.group(2))
times[match.group(1)].append(int(match.group(3)))
elif (match := SRC_PATTERN.search(line)) is not None:
edges.append(int(match.group(2)))
assert len(edges) == 6, edges
assert outputs == EXPECTED
for name, timing in TIMING.items():
lateness = [
at - (edges[edge] + delay)
for at, (edge, delay) in zip(times[name], timing, strict=True)
]
# The source edge is logged in the same millisecond the delay starts, or just after
assert all(-EARLY_MS <= late <= LATE_MS for late in lateness), (name, lateness)
@@ -0,0 +1,119 @@
"""Pin the output of the stateless sensor and text sensor filters, for finite, NaN and infinite inputs."""
from __future__ import annotations
import math
import re
import pytest
from .log_utils import LineWaiter
from .state_utils import require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
INPUTS = [-10.0, 0.0, 0.5, 2.25, 12.345, 50.0, 150.0, math.nan, math.inf]
NAN, INF = math.nan, math.inf
OUT_PATTERN = re.compile(r"OUT (\w+)=([^\s\x1b]+)")
TEXT_PATTERN = re.compile(r"TXT (\w+)=([^\s\x1b]*)")
def _clamp(value: float, low: float | None = None, high: float | None = None) -> float:
# NaN fails every comparison, so it takes the first bound that is set
if low is not None and not low <= value:
return low
if high is not None and not value <= high:
return high
return value
EXPECTED: dict[str, list[float]] = {
"offset_const": [v + 2.5 for v in INPUTS],
"offset_lambda": [v + 1.25 for v in INPUTS],
# The value lambda reads the sensor with id `x` (0.75), not the filter input
"offset_id_x": [v + 0.75 for v in INPUTS],
"multiply_const": [v * 1.5 for v in INPUTS],
# The lambda is `1.0f + 1.0f`, so it must bind as a whole: x * 2, not x * 1 + 1
"multiply_lambda": [v * 2.0 for v in INPUTS],
"clamp_min": [_clamp(v, low=0.0) for v in INPUTS],
"clamp_max": [_clamp(v, high=10.0) for v in INPUTS],
"clamp_both": [_clamp(v, 0.0, 10.0) for v in INPUTS],
"clamp_ignore": [0.0, 0.5, 2.25],
# Technically wrong: b_constant sets c = 0 and the cubic inverse divides by c, so it gives NaN.
# No known use of to_ntc_resistance with b_constant; switch to the Beta inverse if one appears.
"ntc_resistance": [NAN] * len(INPUTS),
# Computed in double from the float rounded coefficients; non-finite inputs give NaN
"ntc_resistance_abc": [
68785.8828,
40731.2266,
39711.0898,
36359.6367,
22255.3613,
4505.29346,
226.547989,
NAN,
NAN,
],
# The inputs are multiplied by 1000 first, so they are resistances in ohms
"ntc_temperature": [
NAN,
NAN,
112.11689,
62.8283424,
20.3332348,
-7.29642916,
-25.600666,
NAN,
NAN,
],
# log(0) is -inf, which the cubic term turns into absolute zero; with c == 0 it is NaN above
"ntc_temperature_abc": [
NAN,
-273.149994,
118.490578,
69.1652069,
25.3175488,
-3.98647237,
-23.8062897,
NAN,
NAN,
],
"calibrate_least_squares": [v * 0.95 + 1.0 / 3.0 for v in INPUTS],
"calibrate_exact": [v * 10.0 if v < 10.0 else v * 5.0 + 50.0 for v in INPUTS[:-2]]
+ [NAN, INF],
"lambda_filter": [v * 2.0 for v in INPUTS if not v > 100.0],
}
TEXT_EXPECTED: dict[str, list[str]] = {
"text_lambda_replace": ["[a]", "[skip]", "[b]"],
"text_lambda_drop": ["a", "b"],
}
@pytest.mark.asyncio
async def test_sensor_filters_apply(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Each copy sensor runs one filter over the same inputs; every output is logged in order."""
waiter = LineWaiter()
async with (
run_compiled(yaml_config, line_callback=waiter.callback),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
done = waiter.expect("FILTERS DONE")
client.button_command(require_entity(entities, "run_filters").key)
await waiter.wait(done, timeout=5.0)
outputs: dict[str, list[float]] = {name: [] for name in EXPECTED}
texts: dict[str, list[str]] = {name: [] for name in TEXT_EXPECTED}
for line in waiter.lines:
if (match := OUT_PATTERN.search(line)) is not None:
outputs[match.group(1)].append(float(match.group(2)))
elif (match := TEXT_PATTERN.search(line)) is not None:
texts[match.group(1)].append(match.group(2))
assert texts == TEXT_EXPECTED
for name, expected in EXPECTED.items():
assert outputs[name] == pytest.approx(expected, rel=1e-5, nan_ok=True), name