mirror of
https://github.com/esphome/esphome.git
synced 2026-11-09 14:45:57 +00:00
Merge branch 'dev' into sx1509-max6956-apply-outputs
This commit is contained in:
66 files changed
+1792
-451
No files matched your search
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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() { ; }
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
AREA_KINDS = ("interference", "detection", "dwell")
|
||||
CONF_LD6004_ID = "ld6004_id"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 {})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user