Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs

# Conflicts:
#	tests/component_tests/output/test_output.py
This commit is contained in:
J. Nick Koston committed 2026-10-30 16:04:53 -10:00
commit e8dc7471a0
208 files changed
+8260 -3410

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.17.2
rev: v0.17.3
hooks:
# Run the linter.
- id: ruff
+1
View File
@@ -355,6 +355,7 @@ esphome/components/ld2450/* @hareeshmu
esphome/components/ld2460/* @jesusvallejo
esphome/components/ld24xx/* @kbx81
esphome/components/ld6002b/* @hepter
esphome/components/ld600x/* @hepter @wolph
esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2
+13 -6
View File
@@ -477,13 +477,20 @@ def has_api() -> bool:
def has_ota() -> bool:
"""Check if any network OTA upload is available.
True if the config exposes either ``platform: esphome`` (native API
OTA) or ``platform: web_server`` (HTTP OTA). Both reach the device
over the same network stack, so the OTA discovery path treats them
interchangeably; ``upload_program`` picks the actual transport based
on ``--ota-platform`` and what's configured.
True if the config exposes ``platform: esphome`` (native API OTA),
``platform: web_server`` (HTTP OTA), or a network OTA that the target
platform uploads to itself (nRF52 SMP over UDP). The OTA discovery path
treats them interchangeably; ``upload_program`` picks the actual transport.
"""
return has_native_ota() or has_web_server_ota()
return has_native_ota() or has_web_server_ota() or has_platform_network_ota()
def has_platform_network_ota() -> bool:
"""Check if the target platform's ``has_network_ota`` hook reports a network OTA."""
if (target := CORE.target_platform) is None:
return False
hook = platform_hooks.get_platform_hook(target, "has_network_ota")
return hook is not None and hook(CORE.config)
def has_native_ota() -> bool:
+2 -1
View File
@@ -1,5 +1,6 @@
#include "adc128s102.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::adc128s102 {
@@ -22,7 +23,7 @@ uint16_t ADC128S102::read_data(uint8_t channel) {
uint8_t adc_secondary_byte = this->transfer_byte(0x00);
this->disable();
uint16_t digital_value = adc_primary_byte << 8 | adc_secondary_byte;
uint16_t digital_value = encode_uint16(adc_primary_byte, adc_secondary_byte);
return digital_value;
}
+3 -5
View File
@@ -11,18 +11,16 @@ namespace esphome::aht10 {
enum AHT10Variant { AHT10, AHT20 };
class AHT10Component final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
void setup() override;
void update() override;
void dump_config() override;
void set_variant(AHT10Variant variant) { this->variant_ = variant; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
AHT10Variant variant_{};
unsigned read_count_{};
void read_data_();
+7 -12
View File
@@ -32,26 +32,21 @@ static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure
static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20;
class Alpha3 final : public esphome::ble_client::BLEClientNode, public PollingComponent {
SUB_SENSOR(flow)
SUB_SENSOR(head)
SUB_SENSOR(power)
SUB_SENSOR(current)
SUB_SENSOR(speed)
SUB_SENSOR(voltage)
public:
void setup() override;
void update() override;
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; }
void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; }
void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; }
protected:
sensor::Sensor *flow_sensor_{nullptr};
sensor::Sensor *head_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *speed_sensor_{nullptr};
sensor::Sensor *voltage_sensor_{nullptr};
uint16_t geni_handle_;
int16_t response_length_;
int16_t response_offset_;
+3 -6
View File
@@ -28,20 +28,17 @@
namespace esphome::am2315c {
class AM2315C final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
void dump_config() override;
void update() override;
void setup() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
protected:
bool convert_(uint8_t *data, float &humidity, float &temperature);
bool reset_register_(uint8_t reg);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace esphome::am2315c
+3 -6
View File
@@ -7,20 +7,17 @@
namespace esphome::am2320 {
class AM2320Component final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
void setup() override;
void dump_config() override;
void update() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
protected:
bool read_data_(uint8_t *data);
bool read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace esphome::am2320
@@ -10,24 +10,19 @@
namespace esphome::as5600 {
class AS5600Sensor final : public PollingComponent, public Parented<AS5600Component>, public sensor::Sensor {
SUB_SENSOR(raw_position)
SUB_SENSOR(gain)
SUB_SENSOR(magnitude)
SUB_SENSOR(status)
public:
void update() override;
void dump_config() override;
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
this->raw_position_sensor_ = raw_position_sensor;
}
void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; }
void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; }
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; }
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
protected:
sensor::Sensor *raw_position_sensor_{nullptr};
sensor::Sensor *gain_sensor_{nullptr};
sensor::Sensor *magnitude_sensor_{nullptr};
sensor::Sensor *status_sensor_{nullptr};
OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX};
};
+2 -1
View File
@@ -1,6 +1,7 @@
#include "atm90e26.h"
#include "atm90e26_reg.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::atm90e26 {
@@ -138,7 +139,7 @@ uint16_t ATM90E26Component::read16_(uint8_t a_register) {
this->read_array(data, 2);
this->disable();
output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
output = encode_uint16(data[0], data[1]);
ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output);
return output;
}
+9 -16
View File
@@ -9,19 +9,20 @@ namespace esphome::atm90e26 {
class ATM90E26Component final : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
SUB_SENSOR(voltage)
SUB_SENSOR(current)
SUB_SENSOR(power)
SUB_SENSOR(reactive_power)
SUB_SENSOR(forward_active_energy)
SUB_SENSOR(reverse_active_energy)
SUB_SENSOR(power_factor)
SUB_SENSOR(freq)
public:
void setup() override;
void dump_config() override;
void update() override;
void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; }
void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; }
void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; }
void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; }
void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; }
void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; }
void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; }
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
void set_line_freq(int freq) { line_freq_ = freq; }
void set_meter_constant(float val) { meter_constant_ = val; }
void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; }
@@ -46,14 +47,6 @@ class ATM90E26Component final : public PollingComponent,
float get_frequency_();
float get_chip_temperature_();
sensor::Sensor *freq_sensor_{nullptr};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
sensor::Sensor *forward_active_energy_sensor_{nullptr};
sensor::Sensor *reverse_active_energy_sensor_{nullptr};
uint32_t cumulative_forward_active_energy_{0};
uint32_t cumulative_reverse_active_energy_{0};
uint16_t gain_metering_{7481};
+4 -3
View File
@@ -1,6 +1,7 @@
#include "bl0939.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
namespace esphome::bl0939 {
@@ -104,7 +105,7 @@ void BL0939::received_package_(const DataPacket *data) const {
PUBLISH_STATE(this->energy_sum_sensor_, total_energy_consumption);
if (this->internal_temperature_sensor_ != nullptr) {
const uint16_t raw_temperature = ((uint16_t(data->tps1.h) << 8) | data->tps1.l) & 0x3FF;
const uint16_t raw_temperature = encode_uint16(data->tps1.h, data->tps1.l) & 0x3FF;
// Datasheet temperature conversion: Tx = (170 / 448) * (TB / 2 - 32) - 45.
const float temperature = (170.0f / 448.0f) * (raw_temperature / 2.0f - 32.0f) - 45.0f;
this->internal_temperature_sensor_->publish_state(temperature);
@@ -132,8 +133,8 @@ void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexit
LOG_SENSOR("", "Internal Temperature", this->internal_temperature_sensor_);
}
uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
uint32_t BL0939::to_uint32_t(ube24_t input) { return encode_uint24(input.h, input.m, input.l); }
int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; }
int32_t BL0939::to_int32_t(sbe24_t input) { return sign_extend<24>(encode_uint24(input.h, input.m, input.l)); }
} // namespace esphome::bl0939
@@ -51,8 +51,6 @@ 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;
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT
switch (filter) {
case BME280_IIR_FILTER_OFF:
@@ -290,7 +288,7 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
}
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
uint16_t const raw_adc = encode_uint16(data[6], data[7]);
if (raw_adc == 0x8000)
return NAN;
+4 -7
View File
@@ -57,11 +57,11 @@ enum BME280IIRFilter {
/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor.
class BME280Component : public PollingComponent {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
SUB_SENSOR(humidity)
public:
/// Set the oversampling value for the temperature sensor. Default is 16x.
void set_temperature_oversampling(BME280Oversampling temperature_over_sampling);
/// Set the oversampling value for the pressure sensor. Default is 16x.
@@ -98,9 +98,6 @@ class BME280Component : public PollingComponent {
BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
BME280Oversampling humidity_oversampling_{BME280_OVERSAMPLING_16X};
BME280IIRFilter iir_filter_{BME280_IIR_FILTER_OFF};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace esphome::bme280_base
+5 -5
View File
@@ -98,7 +98,7 @@ bool BME680Component::setup_registers_() {
this->calibration_.t3 = coeff1[3];
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
this->calibration_.h2 = encode_uint16(coeff2[0], coeff2[1]) >> 4;
this->calibration_.h3 = coeff2[3];
this->calibration_.h4 = coeff2[4];
this->calibration_.h5 = coeff2[5];
@@ -268,10 +268,10 @@ void BME680Component::read_data_() {
return;
}
uint32_t raw_temperature = (uint32_t(data[5]) << 12) | (uint32_t(data[6]) << 4) | (uint32_t(data[7]) >> 4);
uint32_t raw_pressure = (uint32_t(data[2]) << 12) | (uint32_t(data[3]) << 4) | (uint32_t(data[4]) >> 4);
uint32_t raw_humidity = (uint32_t(data[8]) << 8) | uint32_t(data[9]);
uint16_t raw_gas = (uint16_t) ((uint32_t) data[13] * 4 | (((uint32_t) data[14]) / 64));
uint32_t raw_temperature = encode_uint24(data[5], data[6], data[7]) >> 4;
uint32_t raw_pressure = encode_uint24(data[2], data[3], data[4]) >> 4;
uint32_t raw_humidity = encode_uint16(data[8], data[9]);
uint16_t raw_gas = encode_uint16(data[13], data[14]) >> 6;
uint8_t gas_range = data[14] & 0x0F;
float temperature = this->calc_temperature_(raw_temperature);
+5 -12
View File
@@ -66,6 +66,11 @@ struct BME680CalibrationData {
};
class BME680Component final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
SUB_SENSOR(humidity)
SUB_SENSOR(gas_resistance)
public:
/// Set the temperature oversampling value. Defaults to 16X.
void set_temperature_oversampling(BME680Oversampling temperature_oversampling);
@@ -76,13 +81,6 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
/// Set the IIR Filter value. Defaults to no IIR Filter.
void set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
void set_gas_resistance_sensor(sensor::Sensor *gas_resistance_sensor) {
gas_resistance_sensor_ = gas_resistance_sensor;
}
/** Set how the internal heater should operate.
*
* By default, the heater is off. If you want to to have more reliable
@@ -128,11 +126,6 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
BME680IIRFilter iir_filter_{BME680_IIR_FILTER_OFF};
uint16_t heater_temperature_{320};
uint16_t heater_duration_{150};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *gas_resistance_sensor_{nullptr};
};
} // namespace esphome::bme680
+8 -16
View File
@@ -7,29 +7,21 @@
namespace esphome::bmi160 {
class BMI160Component final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(accel_x)
SUB_SENSOR(accel_y)
SUB_SENSOR(accel_z)
SUB_SENSOR(temperature)
SUB_SENSOR(gyro_x)
SUB_SENSOR(gyro_y)
SUB_SENSOR(gyro_z)
public:
void setup() override;
void dump_config() override;
void update() override;
void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; }
void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; }
void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_gyro_x_sensor(sensor::Sensor *gyro_x_sensor) { gyro_x_sensor_ = gyro_x_sensor; }
void set_gyro_y_sensor(sensor::Sensor *gyro_y_sensor) { gyro_y_sensor_ = gyro_y_sensor; }
void set_gyro_z_sensor(sensor::Sensor *gyro_z_sensor) { gyro_z_sensor_ = gyro_z_sensor; }
protected:
sensor::Sensor *accel_x_sensor_{nullptr};
sensor::Sensor *accel_y_sensor_{nullptr};
sensor::Sensor *accel_z_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *gyro_x_sensor_{nullptr};
sensor::Sensor *gyro_y_sensor_{nullptr};
sensor::Sensor *gyro_z_sensor_{nullptr};
void internal_setup_(int stage);
/// Run one setup stage; false on an I2C failure.
bool setup_stage_(int stage);
+12 -12
View File
@@ -30,17 +30,17 @@ void BMP085Component::setup() {
}
// Load calibration
this->calibration_.ac1 = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF);
this->calibration_.ac2 = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF);
this->calibration_.ac3 = ((data[4] & 0xFF) << 8) | (data[5] & 0xFF);
this->calibration_.ac4 = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
this->calibration_.ac5 = ((data[8] & 0xFF) << 8) | (data[9] & 0xFF);
this->calibration_.ac6 = ((data[10] & 0xFF) << 8) | (data[11] & 0xFF);
this->calibration_.b1 = ((data[12] & 0xFF) << 8) | (data[13] & 0xFF);
this->calibration_.b2 = ((data[14] & 0xFF) << 8) | (data[15] & 0xFF);
this->calibration_.mb = ((data[16] & 0xFF) << 8) | (data[17] & 0xFF);
this->calibration_.mc = ((data[18] & 0xFF) << 8) | (data[19] & 0xFF);
this->calibration_.md = ((data[20] & 0xFF) << 8) | (data[21] & 0xFF);
this->calibration_.ac1 = encode_uint16(data[0], data[1]);
this->calibration_.ac2 = encode_uint16(data[2], data[3]);
this->calibration_.ac3 = encode_uint16(data[4], data[5]);
this->calibration_.ac4 = encode_uint16(data[6], data[7]);
this->calibration_.ac5 = encode_uint16(data[8], data[9]);
this->calibration_.ac6 = encode_uint16(data[10], data[11]);
this->calibration_.b1 = encode_uint16(data[12], data[13]);
this->calibration_.b2 = encode_uint16(data[14], data[15]);
this->calibration_.mb = encode_uint16(data[16], data[17]);
this->calibration_.mc = encode_uint16(data[18], data[19]);
this->calibration_.md = encode_uint16(data[20], data[21]);
}
void BMP085Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMP085:");
@@ -59,7 +59,7 @@ void BMP085Component::read_temperature_() {
return;
}
int32_t ut = ((buffer[0] & 0xFF) << 8) | (buffer[1] & 0xFF);
int32_t ut = encode_uint16(buffer[0], buffer[1]);
if (ut == 0) {
ESP_LOGW(TAG, "Invalid temperature!");
this->status_set_warning();
@@ -20,8 +20,6 @@ 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;
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
static const char *oversampling_to_str(BMP280Oversampling oversampling) {
switch (oversampling) {
case BMP280_OVERSAMPLING_NONE:
+3 -5
View File
@@ -50,10 +50,10 @@ enum BMP280IIRFilter {
/// This class implements support for the BMP280 Temperature+Pressure sensor.
class BMP280Component : public PollingComponent {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
public:
/// Set the oversampling value for the temperature sensor. Default is 16x.
void set_temperature_oversampling(BMP280Oversampling temperature_over_sampling);
/// Set the oversampling value for the pressure sensor. Default is 16x.
@@ -83,8 +83,6 @@ class BMP280Component : public PollingComponent {
BMP280Oversampling temperature_oversampling_{BMP280_OVERSAMPLING_16X};
BMP280Oversampling pressure_oversampling_{BMP280_OVERSAMPLING_16X};
BMP280IIRFilter iir_filter_{BMP280_IIR_FILTER_OFF};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
};
} // namespace esphome::bmp280_base
+3 -5
View File
@@ -69,14 +69,14 @@ enum IIRFilter {
/// This class implements support for the BMP3XX Temperature+Pressure sensor.
class BMP3XXComponent : public PollingComponent {
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
public:
void setup() override;
void dump_config() override;
void update() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
/// Set the oversampling value for the temperature sensor. Default is 16x.
void set_temperature_oversampling_config(Oversampling temperature_oversampling) {
this->temperature_oversampling_ = temperature_oversampling;
@@ -122,8 +122,6 @@ class BMP3XXComponent : public PollingComponent {
Oversampling pressure_oversampling_{OVERSAMPLING_X16};
IIRFilter iir_filter_{IIR_FILTER_OFF};
OperationMode operation_mode_{FORCED_MODE};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
struct { // The BMP388 compensation trim parameters (coefficients)
uint16_t param_T1;
+3 -6
View File
@@ -59,15 +59,15 @@ enum IIRFilter {
};
class BMP581Component : public PollingComponent {
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
public:
void dump_config() override;
void setup() override;
void update() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
void set_temperature_oversampling_config(Oversampling temperature_oversampling) {
this->temperature_oversampling_ = temperature_oversampling;
}
@@ -91,9 +91,6 @@ class BMP581Component : public PollingComponent {
// Interface activation function. Only used for SPI interface; no-op for I2C.
virtual void activate_interface() {}
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
Oversampling temperature_oversampling_;
Oversampling pressure_oversampling_;
-3
View File
@@ -25,8 +25,6 @@ void BP5758D::setup() {
this->clock_pin_->setup();
this->clock_pin_->digital_write(false);
delayMicroseconds(BP5758D_DELAY);
this->channel_current_.resize(5, 0);
this->pwm_amounts_.resize(5, 0);
}
void BP5758D::dump_config() {
ESP_LOGCONFIG(TAG, "BP5758D:");
@@ -117,7 +115,6 @@ uint8_t BP5758D::correct_current_level_bits_(uint8_t current) {
void BP5758D::set_channel_value_(uint8_t channel, uint16_t value) {
if (this->pwm_amounts_[channel] != value) {
this->update_ = true;
this->update_channel_ = channel;
}
this->pwm_amounts_[channel] = value;
}
+3 -3
View File
@@ -4,6 +4,7 @@
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>
#include <array>
namespace esphome::bp5758d {
@@ -53,9 +54,8 @@ class BP5758D final : public Component {
GPIOPin *data_pin_;
GPIOPin *clock_pin_;
uint8_t update_channel_;
std::vector<uint8_t> channel_current_;
std::vector<uint16_t> pwm_amounts_;
std::array<uint8_t, 5> channel_current_{};
std::array<uint16_t, 5> pwm_amounts_{};
bool update_{true};
};
+2 -3
View File
@@ -130,9 +130,8 @@ bool BQ76972Component::write_subcommand_(uint16_t subcommand, const uint8_t *dat
return false;
// Inverted 8-bit sum of 0x3E, 0x3F and the data; length counts 0x3E, 0x3F, 0x60, 0x61 and the data
uint8_t sum = static_cast<uint8_t>(subcommand & 0xFF) + static_cast<uint8_t>(subcommand >> 8);
for (size_t i = 0; i < len; ++i)
sum += data[i];
const uint8_t sum =
sum_bytes(data, len, static_cast<uint8_t>(subcommand & 0xFF) + static_cast<uint8_t>(subcommand >> 8));
const uint8_t tail[2] = {static_cast<uint8_t>(~sum), static_cast<uint8_t>(4 + len)};
return this->write_block_(BQ76972_REG_CHECKSUM, tail, 2);
}
+2 -2
View File
@@ -23,7 +23,6 @@ class BQ76972Component final : public PollingComponent, public i2c::I2CDevice {
void set_crc_mode(bool crc_mode) { this->crc_mode_ = crc_mode; }
void set_reg_disable(bool reg_disable) { this->reg_disable_ = reg_disable; }
#ifdef USE_SENSOR
void set_stack_voltage_sensor(sensor::Sensor *s) { this->stack_voltage_sensor_ = s; }
void set_cell_sensor(sensor::Sensor *s, size_t index) { this->cell_sensors_[index] = s; }
void set_temperature_sensor(sensor::Sensor *s, size_t index) { this->temperature_sensors_[index] = s; }
#endif
@@ -49,7 +48,8 @@ class BQ76972Component final : public PollingComponent, public i2c::I2CDevice {
bool crc_mode_{false};
#ifdef USE_SENSOR
sensor::Sensor *stack_voltage_sensor_{nullptr};
SUB_SENSOR(stack_voltage)
protected:
std::array<sensor::Sensor *, BQ76972_NUM_CELLS> cell_sensors_{};
std::array<sensor::Sensor *, BQ76972_NUM_TEMPERATURES> temperature_sensors_{};
#endif
+3 -2
View File
@@ -140,8 +140,9 @@ void CCS811Component::send_env_data_() {
// https://cdn.sparkfun.com/datasheets/BreakoutBoards/CCS811_Programming_Guide.pdf
// Reference code:
// https://github.com/adafruit/Adafruit_CCS811/blob/0990f5c620354d8bc087c4706bec091d8e6e5dfd/Adafruit_CCS811.cpp#L135-L142
uint16_t hum_conv = static_cast<uint16_t>(lroundf(humidity * 512.0f + 0.5f));
uint16_t temp_conv = static_cast<uint16_t>(lroundf(temperature * 512.0f + 0.5f));
// lroundf already rounds to nearest; the reference adds 0.5 only because it truncates.
uint16_t hum_conv = static_cast<uint16_t>(lroundf(humidity * 512.0f));
uint16_t temp_conv = static_cast<uint16_t>(lroundf(temperature * 512.0f));
this->write_bytes(0x05, {(uint8_t) ((hum_conv >> 8) & 0xff), (uint8_t) ((hum_conv & 0xff)),
(uint8_t) ((temp_conv >> 8) & 0xff), (uint8_t) ((temp_conv & 0xff))});
}
+3 -6
View File
@@ -1,4 +1,5 @@
#include "cm1106.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -17,11 +18,7 @@ static constexpr uint8_t C_M1106_CMD_SET_ABC_STATUS_RESPONSE[4] = {0x16, 0x01, 0
static constexpr uint8_t CM1106_ABC_FLAG_ENABLE = 0x0;
static constexpr uint8_t CM1106_ABC_FLAG_DISABLE = 0x2;
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len - 1; i++) {
crc -= response[i];
}
return crc;
return static_cast<uint8_t>(-sum_bytes(response, len - 1));
}
void CM1106Component::setup() {
@@ -57,7 +54,7 @@ void CM1106Component::update() {
return;
}
const uint16_t ppm = response[3] << 8 | response[4];
const uint16_t ppm = encode_uint16(response[3], response[4]);
const uint8_t status = response[5];
if (status & 0x1) {
ESP_LOGW(TAG, "Preheating; CO₂=%uppm", ppm);
+2 -2
View File
@@ -15,6 +15,8 @@ enum CM1106ABCLogic : uint8_t {
};
class CM1106Component final : public PollingComponent, public uart::UARTDevice {
SUB_SENSOR(co2)
public:
void setup() override;
void update() override;
@@ -24,7 +26,6 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
void abc_enable() { this->abc_set_(true); }
void abc_disable() { this->abc_set_(false); }
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
void set_abc(bool enabled, uint8_t cycle_days, uint16_t baseline_ppm) {
this->abc_boot_logic_ = enabled ? CM1106_ABC_ENABLED : CM1106_ABC_DISABLED;
this->abc_cycle_ = cycle_days;
@@ -37,7 +38,6 @@ class CM1106Component final : public PollingComponent, public uart::UARTDevice {
bool send_command_expect_ack_(const uint8_t *command, size_t command_len, const uint8_t *ack);
void abc_set_(bool enabled);
sensor::Sensor *co2_sensor_{nullptr};
CM1106ABCLogic abc_boot_logic_{CM1106_ABC_NONE};
uint8_t abc_cycle_{15};
uint16_t abc_baseline_{400};
+1
View File
@@ -88,6 +88,7 @@ CONF_SOURCES = "sources"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TARGET_COUNT = "target_count"
CONF_TEMPERATURE_SENSOR = "temperature_sensor"
CONF_USE_PSRAM = "use_psram"
CONF_VALUE_TYPE = "value_type"
CONF_VERIFY_SSL = "verify_ssl"
+4 -6
View File
@@ -54,6 +54,10 @@ enum CS5460APGAGain {
class CS5460AComponent final : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
SUB_SENSOR(current)
SUB_SENSOR(voltage)
SUB_SENSOR(power)
public:
void set_samples(uint32_t samples) { samples_ = samples; }
void set_phase_offset(int8_t phase_offset) { phase_offset_ = phase_offset; }
@@ -67,9 +71,6 @@ class CS5460AComponent final : public Component,
voltage_hpf_ = voltage_hpf;
}
void set_pulse_energy_wh(float pulse_energy_wh) { pulse_energy_wh_ = pulse_energy_wh; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void restart() { restart_(); }
@@ -85,9 +86,6 @@ class CS5460AComponent final : public Component,
bool current_hpf_;
bool voltage_hpf_;
float pulse_energy_wh_;
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
void write_register_(enum CS5460ARegister addr, uint32_t value);
uint32_t read_register_(uint8_t addr);
+8 -19
View File
@@ -10,19 +10,15 @@ namespace esphome::cse7766 {
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
class CSE7766Component final : public Component, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) {
apparent_power_sensor_ = apparent_power_sensor;
}
void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) {
reactive_power_sensor_ = reactive_power_sensor;
}
void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; }
SUB_SENSOR(voltage)
SUB_SENSOR(current)
SUB_SENSOR(power)
SUB_SENSOR(energy)
SUB_SENSOR(apparent_power)
SUB_SENSOR(reactive_power)
SUB_SENSOR(power_factor)
public:
void loop() override;
void dump_config() override;
@@ -38,13 +34,6 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
uint8_t raw_data_index_{0};
uint16_t cf_pulses_last_{0};
uint32_t last_transmission_{0};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *energy_sensor_{nullptr};
sensor::Sensor *apparent_power_sensor_{nullptr};
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
// Power cycle of the last frame that carried a new power measurement, 0 if none
uint32_t last_power_cycle_{0};
+2 -2
View File
@@ -151,8 +151,8 @@ uint8_t DaikinBrcClimate::temperature_() {
uint8_t temperature;
// Temperature in remote is in F
if (this->fahrenheit_) {
temperature = (uint8_t) roundf(
clamp<float>(((this->target_temperature * 1.8f) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
temperature = (uint8_t) roundf(clamp<float>(celsius_to_fahrenheit(this->target_temperature),
DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
} else {
temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1;
}
@@ -102,7 +102,7 @@ static bool verify_crc(const uint8_t *data, size_t len) {
if (len < EXCEPTION_RESPONSE_LEN)
return false;
const uint16_t calculated = crc16(data, static_cast<uint16_t>(len - 2));
const uint16_t received = data[len - 2] | (data[len - 1] << 8);
const uint16_t received = encode_uint16(data[len - 1], data[len - 2]);
return calculated == received;
}
@@ -335,7 +335,7 @@ void DeltaWallbox::handle_frame_(const uint8_t *frame, size_t len) {
}
case DeltaState::DELTA_STATE_CHECK_PAIRING: {
if (frame[2] >= REGISTER_BYTE_COUNT) {
uint16_t status = (frame[3] << 8) | frame[4];
uint16_t status = encode_uint16(frame[3], frame[4]);
ESP_LOGI(TAG, "Pairing Status Register: 0x%04X", status);
if (status == PAIRING_STATUS_READY) {
this->pairing_in_progress_ = false;
@@ -464,13 +464,13 @@ void DeltaWallbox::handle_frame_(const uint8_t *frame, size_t len) {
this->send_frame_(CMD_GET_ERROR_CODE, sizeof(CMD_GET_ERROR_CODE));
} else if (byte_count == REGISTER_BYTE_COUNT) {
// Error status register (0x01B5)
uint16_t reg_val = (frame[3] << 8) | frame[4];
uint16_t reg_val = encode_uint16(frame[3], frame[4]);
char hex_str[16];
snprintf(hex_str, sizeof(hex_str), "%04X", reg_val);
PUBLISH_STATE(this->error_code_text_sensor_, hex_str);
}
} else if (frame[1] == FC_WRITE_SINGLE_REGISTER) {
const uint16_t register_addr = (static_cast<uint16_t>(frame[2]) << 8) | frame[3];
const uint16_t register_addr = encode_uint16(frame[2], frame[3]);
if (register_addr == REG_MAX_CURRENT) {
ESP_LOGI(TAG, "Wallbox max current command acknowledged: %u A", frame[5]);
PUBLISH_STATE(this->max_current_number_, frame[5]);
+3 -4
View File
@@ -19,6 +19,9 @@ enum DHTModel : uint8_t {
/// Component for reading temperature/humidity measurements from DHT11/DHT22 sensors.
class DHT final : public PollingComponent {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
/** Manually select the DHT model.
*
@@ -41,8 +44,6 @@ class DHT final : public PollingComponent {
this->t_pin_ = pin;
this->pin_ = pin->to_isr();
}
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
/// Set up the pins and check connection.
void setup() override;
@@ -53,8 +54,6 @@ class DHT final : public PollingComponent {
protected:
bool read_sensor_(float *temperature, float *humidity, bool report_errors);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
InternalGPIOPin *t_pin_;
ISRInternalGPIOPin pin_;
DHTModel model_{DHT_MODEL_AUTO_DETECT};
+3 -6
View File
@@ -7,19 +7,16 @@
namespace esphome::dht12 {
class DHT12Component final : public PollingComponent, public i2c::I2CDevice {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
void setup() override;
void dump_config() override;
void update() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
protected:
bool read_data_(uint8_t *data);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace esphome::dht12
+2 -15
View File
@@ -24,8 +24,7 @@ void EE895Component::setup() {
crc16_check = (serial_number[19] << 8) + serial_number[18];
if (crc16_check != calc_crc16_(serial_number, 18)) {
this->error_code_ = CRC_CHECK_FAILED;
this->mark_failed();
this->mark_failed(LOG_STR("CRC check failed"));
return;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -37,17 +36,6 @@ void EE895Component::setup() {
void EE895Component::dump_config() {
ESP_LOGCONFIG(TAG, "EE895:");
LOG_I2C_DEVICE(this);
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break;
case CRC_CHECK_FAILED:
ESP_LOGE(TAG, "The crc check failed");
break;
case NONE:
default:
break;
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
@@ -92,8 +80,7 @@ float EE895Component::read_float_() {
this->read(i2c_response, 8);
crc16_check = (i2c_response[7] << 8) + i2c_response[6];
if (crc16_check != calc_crc16_(i2c_response, 6)) {
this->error_code_ = CRC_CHECK_FAILED;
this->status_set_warning();
this->status_set_warning(LOG_STR("CRC check failed"));
return 0;
}
uint32_t x = encode_uint32(i2c_response[4], i2c_response[5], i2c_response[2], i2c_response[3]);
-2
View File
@@ -21,8 +21,6 @@ class EE895Component final : public PollingComponent, public i2c::I2CDevice {
void write_command_(uint16_t addr, uint16_t reg_cnt);
float read_float_();
uint16_t calc_crc16_(const uint8_t buf[], uint8_t len);
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
};
} // namespace esphome::ee895
+3 -2
View File
@@ -3,6 +3,7 @@
// - Official Datasheet: https://ww1.microchip.com/downloads/en/DeviceDoc/2101.pdf
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "emc2101.h"
namespace esphome::emc2101 {
@@ -143,7 +144,7 @@ float Emc2101Component::get_external_temperature() {
}
// join msb and lsb (5 least significant bits are not used); msb is signed, so read as int16_t
int16_t raw = static_cast<int16_t>((msb << 8) | lsb) >> 5;
int16_t raw = static_cast<int16_t>(encode_uint16(msb, lsb)) >> 5;
return raw * 0.125f;
}
@@ -157,7 +158,7 @@ float Emc2101Component::get_speed() {
}
// calculate RPMs
uint16_t tach = msb << 8 | lsb;
uint16_t tach = encode_uint16(msb, lsb);
return tach == 0xFFFF ? 0.0f : 5400000.0f / tach;
}
@@ -1,6 +1,7 @@
#include "fingerprint_grow.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
namespace esphome::fingerprint_grow {
@@ -126,8 +127,8 @@ void FingerprintGrowComponent::scan_and_match_() {
switch (this->send_command_()) {
case OK: {
ESP_LOGD(TAG, "Fingerprint matched");
uint16_t finger_id = ((uint16_t) this->data_[1] << 8) | this->data_[2];
uint16_t confidence = ((uint16_t) this->data_[3] << 8) | this->data_[4];
uint16_t finger_id = encode_uint16(this->data_[1], this->data_[2]);
uint16_t confidence = encode_uint16(this->data_[3], this->data_[4]);
PUBLISH_STATE(this->last_finger_id_sensor_, finger_id);
PUBLISH_STATE(this->last_confidence_sensor_, confidence);
this->finger_scan_matched_callback_.call(finger_id, confidence);
@@ -255,11 +256,11 @@ bool FingerprintGrowComponent::get_parameters_() {
if (this->send_command_() == OK) {
ESP_LOGD(TAG, "Got parameters");
// Bear in mind data_[0] is the transfer status, the parameters table start at data_[1]
PUBLISH_STATE(this->status_sensor_, ((uint16_t) this->data_[1] << 8) | this->data_[2]);
this->system_identifier_code_ = ((uint16_t) this->data_[3] << 8) | this->data_[4];
this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6];
PUBLISH_STATE(this->status_sensor_, encode_uint16(this->data_[1], this->data_[2]));
this->system_identifier_code_ = encode_uint16(this->data_[3], this->data_[4]);
this->capacity_ = encode_uint16(this->data_[5], this->data_[6]);
PUBLISH_STATE(this->capacity_sensor_, this->capacity_);
PUBLISH_STATE(this->security_level_sensor_, ((uint16_t) this->data_[7] << 8) | this->data_[8]);
PUBLISH_STATE(this->security_level_sensor_, encode_uint16(this->data_[7], this->data_[8]));
PUBLISH_STATE(this->enrolling_binary_sensor_, false);
this->get_fingerprint_count_();
return true;
@@ -272,7 +273,7 @@ void FingerprintGrowComponent::get_fingerprint_count_() {
this->data_ = {TEMPLATE_COUNT};
if (this->send_command_() == OK) {
ESP_LOGD(TAG, "Got fingerprint count");
PUBLISH_STATE(this->fingerprint_count_sensor_, ((uint16_t) this->data_[1] << 8) | this->data_[2]);
PUBLISH_STATE(this->fingerprint_count_sensor_, encode_uint16(this->data_[1], this->data_[2]));
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
#include "fs3000.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::fs3000 {
@@ -49,7 +50,7 @@ void FS3000Component::update() {
}
// raw value information is 12 bits
uint16_t raw_value = (data[1] << 8) | data[2];
uint16_t raw_value = encode_uint16(data[1], data[2]);
ESP_LOGV(TAG, "Got raw reading=%i", raw_value);
// convert and publish the raw value into m/s using the table of data points in the datasheet
+3 -2
View File
@@ -7,6 +7,7 @@
#include "ft63x6.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
// Registers
// Reference: https://focuslcds.com/content/FT6236.pdf
@@ -95,13 +96,13 @@ uint16_t FT63X6Touchscreen::read_touch_x_(uint8_t touch) {
uint8_t read_buf[2];
read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6));
read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + 1 + (touch * 6));
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
return encode_uint16(read_buf[0] & 0x0f, read_buf[1]);
}
uint16_t FT63X6Touchscreen::read_touch_y_(uint8_t touch) {
uint8_t read_buf[2];
read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + (touch * 6));
read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + 1 + (touch * 6));
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
return encode_uint16(read_buf[0] & 0x0f, read_buf[1]);
}
uint8_t FT63X6Touchscreen::read_touch_event_(uint8_t touch) {
return this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)) >> 6;
+2 -1
View File
@@ -1,4 +1,5 @@
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "hm3301.h"
namespace esphome::hm3301 {
@@ -83,7 +84,7 @@ void HM3301Component::update() {
bool HM3301Component::validate_checksum_(const uint8_t *data) { return sum_bytes(data, 28) == data[28]; }
uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) {
return (uint16_t) data[i * 2] << 8 | data[i * 2 + 1];
return encode_uint16(data[i * 2], data[i * 2 + 1]);
}
} // namespace esphome::hm3301
@@ -4,6 +4,7 @@
#include "honeywell_hih.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::honeywell_hih_i2c {
@@ -22,10 +23,10 @@ void HoneywellHIComponent::read_sensor_data_() {
return;
}
const uint16_t raw_humidity = (static_cast<uint16_t>(data[0] & 0x3F) << 8) | data[1];
const uint16_t raw_humidity = encode_uint16(data[0] & 0x3F, data[1]);
float humidity = (static_cast<float>(raw_humidity) / MAX_COUNT) * 100;
const uint16_t raw_temperature = (static_cast<uint16_t>(data[2]) << 6) | (data[3] >> 2);
const uint16_t raw_temperature = encode_uint16(data[2], data[3]) >> 2;
float temperature = (static_cast<float>(raw_temperature) / MAX_COUNT) * 165 - 40;
ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
+16 -25
View File
@@ -11,10 +11,12 @@ static constexpr size_t HTE501_SERIAL_NUMBER_SIZE = 7;
void HTE501Component::setup() {
uint8_t address[] = {0x70, 0x29};
uint8_t identification[9];
this->write_read(address, sizeof address, identification, sizeof identification);
if (this->write_read(address, sizeof address, identification, sizeof identification) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
this->mark_failed();
this->mark_failed(LOG_STR("CRC check failed"));
return;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -26,17 +28,6 @@ void HTE501Component::setup() {
void HTE501Component::dump_config() {
ESP_LOGCONFIG(TAG, "HTE501:");
LOG_I2C_DEVICE(this);
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break;
case CRC_CHECK_FAILED:
ESP_LOGE(TAG, "The crc check failed");
break;
case NONE:
default:
break;
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -44,22 +35,22 @@ void HTE501Component::dump_config() {
void HTE501Component::update() {
uint8_t address_1[] = {0x2C, 0x1B};
this->write(address_1, 2);
if (this->write(address_1, 2) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->set_timeout(50, [this]() {
uint8_t i2c_response[6];
this->read(i2c_response, 6);
if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) ||
i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED;
this->status_set_warning();
if (this->read(i2c_response, 6) != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
float temperature = (float) encode_uint16(i2c_response[0], i2c_response[1]);
if (temperature > 55536) {
temperature = (temperature - 65536) / 100;
} else {
temperature = temperature / 100;
if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) ||
i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) {
this->status_set_warning(LOG_STR("CRC check failed"));
return;
}
float temperature = static_cast<int16_t>(encode_uint16(i2c_response[0], i2c_response[1])) / 100.0f;
float humidity = ((float) encode_uint16(i2c_response[3], i2c_response[4])) / 100.0f;
ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
-3
View File
@@ -15,9 +15,6 @@ class HTE501Component final : public PollingComponent, public i2c::I2CDevice {
void setup() override;
void dump_config() override;
void update() override;
protected:
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
};
} // namespace esphome::hte501
+3 -2
View File
@@ -1,6 +1,7 @@
#include "hyt271.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::hyt271 {
@@ -32,8 +33,8 @@ void HYT271Component::update() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
return;
}
uint16_t raw_temperature = ((raw_data[2] << 8) | raw_data[3]) >> 2;
uint16_t raw_humidity = ((raw_data[0] & 0x3F) << 8) | raw_data[1];
uint16_t raw_temperature = encode_uint16(raw_data[2], raw_data[3]) >> 2;
uint16_t raw_humidity = encode_uint16(raw_data[0] & 0x3F, raw_data[1]);
float temperature = ((float(raw_temperature)) * (165.0f / 16383.0f)) - 40.0f;
float humidity = (float(raw_humidity)) * (100.0f / 16383.0f);
@@ -206,7 +206,7 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
return;
}
uint16_t recv_command = msg[2] << 8 | msg[3];
uint16_t recv_command = encode_uint16(msg[2], msg[3]);
if (recv_command != command) {
ESP_LOGE(TAG, "Received invalid message (invalid unexpected command received 0x%04X, expected 0x%04X)",
recv_command, command);
+1 -1
View File
@@ -192,7 +192,7 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va
snprintf(buf, sizeof(buf), "CRC error reading from register 0x%02X", register_to_read);
this->status_set_warning(buf);
} else {
*register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3];
*register_value = encode_uint16(read_buffer[4], read_buffer[3]);
return i2c::NO_ERROR;
}
}
+14 -48
View File
@@ -1,20 +1,26 @@
from esphome import pins
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,
)
import esphome.config_validation as cv
from esphome.const import CONF_PLATFORM, CONF_WAKEUP_PIN
import esphome.final_validate as fv
from esphome.const import CONF_WAKEUP_PIN
from esphome.types import ConfigType
from .const import CONF_AREA_CONFIG, CONF_AUTO_WAKE, CONF_LD6002B_ID, CONF_WAKEUP_PULSE
from .const import CONF_AUTO_WAKE, CONF_WAKEUP_PULSE
AUTO_LOAD = ["ld600x"]
CODEOWNERS = ["@hepter"]
DEPENDENCIES = ["uart"]
DOMAIN = "ld6002b"
MULTI_CONF = True
ld6002b_ns = cg.esphome_ns.namespace("ld6002b")
LD6002BComponent = ld6002b_ns.class_("LD6002BComponent", cg.Component, uart.UARTDevice)
LD6002BComponent = ld6002b_ns.class_("LD6002BComponent", LD600XComponent)
def _validate_wakeup_options(config: ConfigType) -> ConfigType:
@@ -37,66 +43,26 @@ def _validate_wakeup_options(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(
_validate_wakeup_options,
cv.Schema(
ld600x_hub_schema(LD6002BComponent).extend(
{
cv.GenerateID(): cv.declare_id(LD6002BComponent),
cv.Optional(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema,
cv.Optional(
CONF_WAKEUP_PULSE, default="50ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_AUTO_WAKE, default=True): cv.boolean,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
),
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"ld6002b",
baud_rate=115200,
require_tx=True,
require_rx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
FINAL_VALIDATE_SCHEMA = uart_final_validate("ld6002b")
async def to_code(config: ConfigType) -> None:
var = await uart.new_uart_component(config)
request_model_sizes(var, area_kinds=2)
await cg.add_pin(var.set_wakeup_pin, config.get(CONF_WAKEUP_PIN))
cg.add(var.set_wakeup_pulse_ms(config[CONF_WAKEUP_PULSE].total_milliseconds))
cg.add(var.set_auto_wake(config[CONF_AUTO_WAKE]))
def validate_unique_settings(domain: str, config: ConfigType) -> None:
"""Reject a setting set in two platform blocks of one hub, which keeps one entity per setting."""
hub_id = config[CONF_LD6002B_ID]
entries = [
entry
for entry in fv.full_config.get().get(domain, [])
if entry.get(CONF_PLATFORM) == DOMAIN and entry.get(CONF_LD6002B_ID) == hub_id
]
for key, value in config.items():
if key in (CONF_PLATFORM, CONF_LD6002B_ID):
continue
# area_config groups six settings, so each one is checked on its own
for path in (
[[key, sub] for sub in value] if key == CONF_AREA_CONFIG else [[key]]
):
if sum(_has_path(entry, path) for entry in entries) > 1:
raise cv.Invalid(
f"{'.'.join(path)} is already set for this ld6002b in another {domain} block",
path=path,
)
def _has_path(entry: ConfigType, path: list[str]) -> bool:
for key in path:
if key not in entry:
return False
entry = entry[key]
return True
+4 -45
View File
@@ -1,54 +1,13 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_TARGET, DEVICE_CLASS_OCCUPANCY
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from . import LD6002BComponent
from .const import AREA_COUNT, CONF_LD6002B_ID, MAX_TARGETS
from .const import CONF_LD6002B_ID
DEPENDENCIES = ["ld6002b"]
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
),
}
)
.extend(
{
cv.Optional(f"target_{i + 1}"): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
)
for i in range(MAX_TARGETS)
}
)
.extend(
{
cv.Optional(f"detection_area_{i}"): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
)
for i in range(AREA_COUNT)
}
)
)
CONFIG_SCHEMA = entities.binary_sensor_schema(LD6002BComponent, CONF_LD6002B_ID)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_TARGET, hub.set_presence_binary_sensor)
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
sens = await binary_sensor.new_binary_sensor(target_config)
cg.add(hub.set_target_presence_binary_sensor(i, sens))
for i in range(AREA_COUNT):
if area_config := config.get(f"detection_area_{i}"):
sens = await binary_sensor.new_binary_sensor(area_config)
cg.add(hub.set_area_presence_binary_sensor(i, sens))
await entities.binary_sensor_to_code(config, CONF_LD6002B_ID)
+25 -145
View File
@@ -1,164 +1,44 @@
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import button
from esphome.components.select import DOMAIN as SELECT_DOMAIN
from esphome.components.ld600x import entities
import esphome.config_validation as cv
from esphome.const import (
CONF_AREA_ID,
CONF_ID,
CONF_WAKEUP_PIN,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from esphome.cpp_generator import ParameterListExpression
from esphome.const import CONF_WAKEUP_PIN
from esphome.core import ID
import esphome.final_validate as fv
from esphome.types import ConfigType
from .. import LD6002BComponent, ld6002b_ns
from ..const import (
CONF_APPLY_AREA,
CONF_AUTO_INTERFERENCE,
CONF_CLEAR_INTERFERENCE,
CONF_GET_AREAS,
CONF_GET_DELAY,
CONF_GET_INSTALLATION,
CONF_GET_LOW_POWER_MODE,
CONF_GET_LOW_POWER_SLEEP_TIME,
CONF_GET_SENSITIVITY,
CONF_GET_TRIGGER_SPEED,
CONF_GET_Z_RANGE,
CONF_LD6002B_ID,
CONF_RESET_DETECTION_AREA,
CONF_RESET_UNATTENDED,
CONF_WAKE,
)
from ..const import CONF_LD6002B_ID, CONF_WAKE
DEPENDENCIES = ["ld6002b"]
ButtonType = ld6002b_ns.enum("ButtonType", is_class=True)
LD6002BButtonType = ld6002b_ns.enum("LD6002BButtonType")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_APPLY_AREA): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_AUTO_INTERFERENCE): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_GET_AREAS): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_CLEAR_INTERFERENCE): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_RESET_DETECTION_AREA): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_GET_DELAY): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_SENSITIVITY): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_TRIGGER_SPEED): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_Z_RANGE): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_INSTALLATION): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_LOW_POWER_MODE): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_GET_LOW_POWER_SLEEP_TIME): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_RESET_UNATTENDED): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_WAKE): button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
CONFIG_SCHEMA = entities.button_schema(
LD6002BComponent,
CONF_LD6002B_ID,
extra={cv.Optional(CONF_WAKE): entities.DIAGNOSTIC_BUTTON_SCHEMA},
)
_shared_final_validate = entities.button_final_validate(CONF_LD6002B_ID, "ld6002b")
def final_validate(config: ConfigType) -> None:
full_config = fv.full_config.get()
hub_id = config[CONF_LD6002B_ID]
if config.get(CONF_APPLY_AREA):
has_area_id_select = any(
entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_AREA_ID) is not None
for entry in full_config.get(SELECT_DOMAIN, [])
def _final_validate(config: ConfigType) -> None:
_shared_final_validate(config)
if not config.get(CONF_WAKE):
return
hub_id: ID = config[CONF_LD6002B_ID]
hub_config = fv.get_declaring_config(hub_id, fv.full_config.get())
if hub_config.get(CONF_WAKEUP_PIN) is None:
raise cv.Invalid(
f"{CONF_WAKE} requires {CONF_WAKEUP_PIN} on the parent ld6002b component",
path=[CONF_WAKE],
)
if not has_area_id_select:
raise cv.Invalid(
f"{CONF_APPLY_AREA} requires select.area_id for the same ld6002b instance",
path=[CONF_APPLY_AREA],
)
if config.get(CONF_WAKE):
hub_config = fv.get_declaring_config(hub_id, full_config)
if hub_config.get(CONF_WAKEUP_PIN) is None:
raise cv.Invalid(
f"{CONF_WAKE} requires {CONF_WAKEUP_PIN} on the parent ld6002b component",
path=[CONF_WAKE],
)
FINAL_VALIDATE_SCHEMA = final_validate
BUTTON_MAP = {
CONF_APPLY_AREA: ButtonType.APPLY_AREA,
CONF_AUTO_INTERFERENCE: ButtonType.AUTO_INTERFERENCE,
CONF_GET_AREAS: ButtonType.GET_AREAS,
CONF_CLEAR_INTERFERENCE: ButtonType.CLEAR_INTERFERENCE,
CONF_RESET_DETECTION_AREA: ButtonType.RESET_DETECTION_AREA,
CONF_GET_DELAY: ButtonType.GET_DELAY,
CONF_GET_SENSITIVITY: ButtonType.GET_SENSITIVITY,
CONF_GET_TRIGGER_SPEED: ButtonType.GET_TRIGGER_SPEED,
CONF_GET_Z_RANGE: ButtonType.GET_Z_RANGE,
CONF_GET_INSTALLATION: ButtonType.GET_INSTALLATION,
CONF_GET_LOW_POWER_MODE: ButtonType.GET_LOW_POWER_MODE,
CONF_GET_LOW_POWER_SLEEP_TIME: ButtonType.GET_LOW_POWER_SLEEP_TIME,
CONF_RESET_UNATTENDED: ButtonType.RESET_UNATTENDED,
CONF_WAKE: ButtonType.WAKE,
}
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
confs = [
(button_type, conf)
for key, button_type in BUTTON_MAP.items()
if (conf := config.get(key)) is not None
]
if not confs:
return
hub = await cg.get_variable(config[CONF_LD6002B_ID])
params = button.APPLY_ENTITY.params
# One function for all buttons, picking the type by pointer: one class instead of one per button.
# Named after the first button so two button blocks on one hub do not clash.
fn = f"esphome__{confs[0][1][CONF_ID].id}__button_fn"
cg.add_global(
cg.RawStatement(f"static void {fn}({ParameterListExpression(*params)});")
)
template = cg.TemplateArguments(cg.RawExpression(fn))
created = [
(button_type, await button.new_button(conf, template))
for button_type, conf in confs
]
*head, (last_type, _) = created
chain = "".join(f"entity == {parent_ref(var)} ? {t} : " for t, var in head)
cg.static_function(
fn,
cg.void,
list(params),
[
f"const auto type = {chain}{last_type};",
f"{parent_ref(hub)}->press_button(type);",
],
await entities.button_to_code(
config,
CONF_LD6002B_ID,
extra={CONF_WAKE: LD6002BButtonType.BUTTON_WAKE},
)
+1 -36
View File
@@ -1,41 +1,6 @@
CONF_APPLY_AREA = "apply_area"
CONF_AREA_CONFIG = "area_config"
CONF_AUTO_INTERFERENCE = "auto_interference"
AREA_KINDS = ("interference", "detection")
CONF_AUTO_WAKE = "auto_wake"
CONF_CLEAR_INTERFERENCE = "clear_interference"
CONF_CLUSTER_ID = "cluster_id"
CONF_DOPPLER_INDEX = "doppler_index"
CONF_GET_AREAS = "get_areas"
CONF_GET_DELAY = "get_delay"
CONF_GET_INSTALLATION = "get_installation"
CONF_GET_LOW_POWER_MODE = "get_low_power_mode"
CONF_GET_LOW_POWER_SLEEP_TIME = "get_low_power_sleep_time"
CONF_GET_SENSITIVITY = "get_sensitivity"
CONF_GET_TRIGGER_SPEED = "get_trigger_speed"
CONF_GET_Z_RANGE = "get_z_range"
CONF_HOLD_DELAY = "hold_delay"
CONF_INSTALLATION_MODE = "installation_mode"
CONF_LD6002B_ID = "ld6002b_id"
CONF_LOW_POWER = "low_power"
CONF_LOW_POWER_SLEEP_TIME = "low_power_sleep_time"
CONF_OTA_VERSION = "ota_version"
CONF_POINT_CLOUD = "point_cloud"
CONF_POINT_COUNT = "point_count"
CONF_RESET_DETECTION_AREA = "reset_detection_area"
CONF_RESET_UNATTENDED = "reset_unattended"
CONF_TARGET_DISPLAY = "target_display"
CONF_TRIGGER_SPEED = "trigger_speed"
CONF_WAKE = "wake"
CONF_WAKEUP_PULSE = "wakeup_pulse"
CONF_WORK_MODE = "work_mode"
CONF_Z = "z"
CONF_Z_MAX = "z_max"
CONF_Z_MIN = "z_min"
KEY_X_MIN = "x_min"
KEY_X_MAX = "x_max"
KEY_Y_MIN = "y_min"
KEY_Y_MAX = "y_max"
AREA_COUNT = 4
MAX_TARGETS = 3
File diff suppressed because it is too large. Load diff
+21 -411
View File
@@ -1,450 +1,60 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/components/ld600x/ld600x.h"
#include "esphome/core/gpio.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <array>
#include <cmath>
namespace esphome::ld6002b {
static constexpr uint8_t MAX_TARGETS = 3;
static constexpr uint8_t AREA_COUNT = 4;
// Interference areas own ids 0..AREA_COUNT-1 and detection areas the next four, so
// this is the whole id space TYPE_SET_AREA accepts.
static constexpr uint8_t AREA_ID_COUNT = AREA_COUNT * 2;
static constexpr size_t DEFAULT_MAX_DATA_LEN = 1024;
static constexpr size_t DEFAULT_MAX_DATA_LEN_POINT_CLOUD = 4096;
// Largest protocol payload is TYPE_SET_AREA: int32 area id + 6 floats = 28 bytes.
static constexpr size_t CMD_MAX_DATA_LEN = 28;
enum class ButtonType : uint8_t {
APPLY_AREA,
AUTO_INTERFERENCE,
GET_AREAS,
CLEAR_INTERFERENCE,
RESET_DETECTION_AREA,
GET_DELAY,
GET_SENSITIVITY,
GET_TRIGGER_SPEED,
GET_Z_RANGE,
GET_INSTALLATION,
GET_LOW_POWER_MODE,
GET_LOW_POWER_SLEEP_TIME,
RESET_UNATTENDED,
WAKE,
enum LD6002BButtonType : uint8_t {
BUTTON_WAKE = ld600x::LD600X_MODEL_KIND_BASE,
};
#ifdef USE_SENSOR
struct TargetSensors {
sensor::Sensor *x{nullptr};
sensor::Sensor *y{nullptr};
sensor::Sensor *z{nullptr};
sensor::Sensor *dop_idx{nullptr};
sensor::Sensor *cluster_id{nullptr};
};
struct AreaSensors {
sensor::Sensor *x_min{nullptr};
sensor::Sensor *x_max{nullptr};
sensor::Sensor *y_min{nullptr};
sensor::Sensor *y_max{nullptr};
sensor::Sensor *z_min{nullptr};
sensor::Sensor *z_max{nullptr};
};
#endif
struct AreaConfig {
float x_min{NAN};
float x_max{NAN};
float y_min{NAN};
float y_max{NAN};
float z_min{NAN};
float z_max{NAN};
};
struct VersionPref {
char value[20];
};
class LD6002BComponent : public Component, public uart::UARTDevice {
class LD6002BComponent final : public ld600x::LD600XComponent {
public:
LD6002BComponent() : LD600XComponent(LOG_STR("ld6002b")) {}
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_wakeup_pin(GPIOPin *pin) { this->wakeup_pin_ = pin; }
void set_wakeup_pulse_ms(uint32_t ms) { this->wakeup_pulse_ms_ = ms; }
void set_auto_wake(bool enable) { this->auto_wake_ = enable; }
#ifdef USE_SENSOR
SUB_SENSOR(target_count)
SUB_SENSOR(point_count)
void set_target_x_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
return;
this->targets_[target].x = sensor;
}
void set_target_y_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
return;
this->targets_[target].y = sensor;
}
void set_target_z_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
return;
this->targets_[target].z = sensor;
}
void set_target_dop_idx_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
return;
this->targets_[target].dop_idx = sensor;
}
void set_target_cluster_id_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= MAX_TARGETS)
return;
this->targets_[target].cluster_id = sensor;
}
void set_interference_area_x_min_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->interference_areas_[area].x_min = sensor;
}
void set_interference_area_x_max_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->interference_areas_[area].x_max = sensor;
}
void set_interference_area_y_min_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->interference_areas_[area].y_min = sensor;
}
void set_interference_area_y_max_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->interference_areas_[area].y_max = sensor;
}
void set_interference_area_z_min_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->interference_areas_[area].z_min = sensor;
}
void set_interference_area_z_max_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->interference_areas_[area].z_max = sensor;
}
void set_detection_area_x_min_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->detection_areas_[area].x_min = sensor;
}
void set_detection_area_x_max_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->detection_areas_[area].x_max = sensor;
}
void set_detection_area_y_min_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->detection_areas_[area].y_min = sensor;
}
void set_detection_area_y_max_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->detection_areas_[area].y_max = sensor;
}
void set_detection_area_z_min_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->detection_areas_[area].z_min = sensor;
}
void set_detection_area_z_max_sensor(uint8_t area, sensor::Sensor *sensor) {
if (area >= AREA_COUNT)
return;
this->detection_areas_[area].z_max = sensor;
}
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(presence)
void set_target_presence_binary_sensor(uint8_t target, binary_sensor::BinarySensor *sensor) {
if (target >= MAX_TARGETS)
return;
this->target_presence_[target] = sensor;
}
void set_area_presence_binary_sensor(uint8_t area, binary_sensor::BinarySensor *sensor) {
if (area >= AREA_COUNT)
return;
this->area_presence_[area] = sensor;
}
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(work_mode)
SUB_TEXT_SENSOR(ota_version)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(hold_delay)
SUB_NUMBER(z_min)
SUB_NUMBER(z_max)
SUB_NUMBER(low_power_sleep)
SUB_NUMBER(area_x_min)
SUB_NUMBER(area_x_max)
SUB_NUMBER(area_y_min)
SUB_NUMBER(area_y_max)
SUB_NUMBER(area_z_min)
SUB_NUMBER(area_z_max)
#endif
#ifdef USE_SELECT
SUB_SELECT(sensitivity)
SUB_SELECT(trigger_speed)
SUB_SELECT(installation)
SUB_SELECT(area_id)
#endif
#ifdef USE_SWITCH
SUB_SWITCH(low_power)
SUB_SWITCH(point_cloud)
SUB_SWITCH(target_display)
void set_switch_state(switch_::Switch *entity, bool state) override;
#endif
#ifdef USE_NUMBER
void set_number_value(number::Number *entity, float value);
#ifdef USE_BUTTON
void press_button(uint8_t kind) override;
#endif
#ifdef USE_SELECT
void set_select_value(select::Select *entity, size_t index);
#endif
#ifdef USE_SWITCH
void set_switch_state(switch_::Switch *entity, bool state);
#endif
void press_button(ButtonType type);
protected:
enum class ParseState : uint8_t { SOF, HEADER, HCK, DATA, DCK, DISCARD };
bool handle_model_report(uint16_t type, const uint8_t *data, uint16_t len) override;
bool model_wants_target_stream() const override;
void on_presence_update() override { this->update_work_mode_fallback_(); }
bool defer_frame_write(uint16_t type, const uint8_t *data, uint8_t len, bool track) override;
void setup_model() override;
void dump_model_config() override;
struct PendingCommand {
uint16_t type{0};
uint8_t len{0};
std::array<uint8_t, CMD_MAX_DATA_LEN> data{};
};
void parse_byte_(uint8_t byte);
void reset_parser_();
void handle_frame_(uint16_t type, const uint8_t *data, uint16_t len);
void handle_target_report_(const uint8_t *data, uint16_t len);
void handle_point_cloud_(const uint8_t *data, uint16_t len);
void handle_area_presence_(const uint8_t *data, uint16_t len);
void handle_area_report_(bool interference, const uint8_t *data, uint16_t len);
void handle_delay_report_(const uint8_t *data, uint16_t len);
void handle_sensitivity_report_(const uint8_t *data, uint16_t len);
void handle_trigger_speed_report_(const uint8_t *data, uint16_t len);
void handle_z_range_report_(const uint8_t *data, uint16_t len);
void handle_installation_report_(const uint8_t *data, uint16_t len);
void handle_low_power_report_(const uint8_t *data, uint16_t len);
void handle_low_power_sleep_report_(const uint8_t *data, uint16_t len);
void handle_work_mode_report_(const uint8_t *data, uint16_t len);
void handle_version_report_(const uint8_t *data, uint16_t len);
void update_work_mode_fallback_();
void publish_work_mode_(bool low_power);
// Drops every target-derived reading and the slot table they are indexed by.
void clear_target_state_();
void clear_area_presence_();
void restore_deferred_edits_();
void publish_area_numbers_();
#ifdef USE_SENSOR
void clear_target_slot_(uint8_t index);
#endif
#ifdef USE_NUMBER
void publish_number_clamped_(number::Number *number, float value);
#endif
void update_area_numbers_(const AreaConfig &area);
void update_area_numbers_for_id_(uint8_t area_id);
bool queue_area_config_(uint8_t area_id, const AreaConfig &desired);
void try_apply_pending_area_(bool reported_interference);
void init_area_id_pref_();
void save_area_id_pref_(uint8_t value);
void init_version_pref_();
void save_version_pref_(const char *value);
// Returns whether the command was queued: it is dropped, with a log line, when
// the payload is too long or the ring is full.
bool queue_command_(uint16_t type, const uint8_t *data, uint8_t len);
void process_command_queue_();
void send_command_(uint16_t type, const uint8_t *data, uint8_t len);
void send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
// Returns whether the command reached the queue; see queue_command_.
bool send_control_command_(uint32_t command);
void send_z_range_();
void apply_area_config_();
void wake_();
static float read_f32_le(const uint8_t *data);
static void write_u32_le(uint8_t *data, uint32_t value);
static void write_int32_le(uint8_t *data, int32_t value);
static void write_f32_le(uint8_t *data, float value);
#ifdef USE_SENSOR
std::array<TargetSensors, MAX_TARGETS> targets_{};
std::array<AreaSensors, AREA_COUNT> interference_areas_{};
std::array<AreaSensors, AREA_COUNT> detection_areas_{};
#endif
#ifdef USE_BINARY_SENSOR
std::array<binary_sensor::BinarySensor *, MAX_TARGETS> target_presence_{};
std::array<binary_sensor::BinarySensor *, AREA_COUNT> area_presence_{};
#endif
#ifdef USE_TEXT_SENSOR
ESPPreferenceObject version_pref_{};
bool version_pref_initialized_{false};
#endif
#ifdef USE_SELECT
ESPPreferenceObject area_id_pref_{};
bool area_id_pref_initialized_{false};
#endif
// Keyed so a repeated press replaces its own pending timeout instead of stacking
// another, and so the command path can cancel it when it takes the pin over.
static constexpr uint32_t WAKE_BUTTON_TIMEOUT_ID = MODEL_TIMEOUT_ID;
GPIOPin *wakeup_pin_{nullptr};
uint32_t wakeup_pulse_ms_{50};
bool auto_wake_{true};
ParseState parse_state_{ParseState::SOF};
uint8_t header_pos_{0};
uint8_t header_xor_{0};
uint16_t data_len_{0};
uint16_t frame_type_{0};
uint16_t frame_id_{0};
uint16_t data_pos_{0};
uint8_t data_xor_{0};
uint32_t discard_remaining_{0};
bool frame_oversize_{false};
size_t max_data_len_{0};
uint8_t *data_buf_{nullptr};
uint16_t next_frame_id_{0};
// Sized for the two bursts that reach it, both counted as what is still queued
// once the first command is dequeued: boot leaves 11 with every platform
// configured, and pressing all fourteen buttons before an ack lands leaves 15.
// Neither overflowed 16, but one free slot is not headroom, and overflowing is a
// dropped command with only a log line to show for it. Costs 256 bytes more per
// configured instance, and this component is MULTI_CONF.
static constexpr uint8_t CMD_QUEUE_SIZE = 24;
static constexpr uint32_t CMD_ACK_TIMEOUT_MS = 300;
// A sleeping module consumes the first frame to wake and answers only the one after it.
static constexpr uint32_t CMD_FIRST_ACK_TIMEOUT_MS = 600;
// How long the module stays awake after any frame, and so still answers the next one.
static constexpr uint32_t MODULE_AWAKE_MS = 10000;
static constexpr uint8_t CMD_MAX_RETRIES = 3;
// Keyed so a repeated press replaces its own pending timeout instead of stacking
// another, and so the command path can cancel it when it takes the pin over.
static constexpr uint32_t WAKE_BUTTON_TIMEOUT_ID = 0;
// Keyed so a burst of writes collapses to one read once they settle, rather than
// one read per write.
static constexpr uint32_t AREA_REFRESH_TIMEOUT_ID = 1;
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
StaticRingBuffer<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_;
bool command_active_{false};
bool command_sent_{false};
PendingCommand active_command_{};
// Frame a pending wake pulse will write, snapshotted because active_command_ may move on first.
std::array<uint8_t, CMD_MAX_DATA_LEN> wake_scratch_{};
std::array<uint8_t, ld600x::CMD_MAX_DATA_LEN> wake_scratch_{};
bool auto_wake_{true};
bool wake_pulse_pending_{false};
uint8_t retries_left_{0};
uint32_t last_send_ms_{0};
// Last frame seen in either direction; any traffic keeps the module awake.
uint32_t last_traffic_ms_{0};
// Frames transmitted for the command in flight, including retries; drives the retry budget.
uint8_t attempts_sent_{0};
// Subset of those the module can actually answer: a frame that woke it is consumed, not replied to.
uint8_t acks_expected_{0};
// ACKs still owed for superseded attempts; they carry no id, only their arrival order.
uint16_t stale_ack_type_{0};
uint8_t stale_ack_count_{0};
// When that debt was booked, so a debt no reply can still settle expires instead of eating a live ACK.
uint32_t stale_ack_ms_{0};
// Bumped whenever the active command changes, so a deferred send can tell it was retired.
uint8_t send_generation_{0};
float z_min_{NAN};
float z_max_{NAN};
float area_x_min_{NAN};
float area_x_max_{NAN};
float area_y_min_{NAN};
float area_y_max_{NAN};
float area_z_min_{NAN};
float area_z_max_{NAN};
// What the user has typed and not yet applied; NaN per axis means "nothing of
// mine here, take the module's value". Same sentinel shape as
// pending_area_updates_. Exactly two things empty it: the area_id select moving
// to another area, and an apply that was accepted. A write the bounds guard
// refused leaves it alone, and a deferred apply that had to be dropped hands its
// staged values back here -- but only while the user is still on the area they
// were staged for. Either way the values stay the user's to fix.
AreaConfig area_edits_{};
std::array<AreaConfig, AREA_COUNT> interference_area_values_{};
std::array<AreaConfig, AREA_COUNT> detection_area_values_{};
uint8_t area_id_{0xFF};
bool area_id_set_{false};
// Which person owns each target_N slot, so a slot survives the module re-sorting its array.
std::array<int32_t, MAX_TARGETS> slot_cluster_{};
std::array<bool, MAX_TARGETS> slot_occupied_{};
bool target_presence_any_{false};
// What the switches and setup asked the module for, which is not the same as
// what it is doing yet: a stream keeps sending until it acts on the command.
// The report handlers read these and drop anything a stopped stream still emits.
bool target_display_enabled_{false};
bool point_cloud_enabled_{false};
bool area_presence_any_{false};
bool area_write_in_flight_{false};
bool work_mode_reported_{false};
bool low_power_enabled_{false};
bool low_power_reported_{false};
bool deferred_apply_pending_{false};
uint8_t pending_area_id_{0xFF};
AreaConfig pending_area_updates_{};
bool last_work_mode_valid_{false};
bool last_work_mode_low_power_{false};
#ifdef USE_SENSOR
std::array<bool, MAX_TARGETS> last_target_presence_{}; // one-shot NAN clear for target sensors
// A cluster id names a person, so like the counts it is published on change, not per frame.
std::array<int32_t, MAX_TARGETS> last_cluster_id_{};
std::array<bool, MAX_TARGETS> last_cluster_id_valid_{};
uint32_t last_target_count_{0xFFFFFFFF};
uint32_t last_point_count_{0xFFFFFFFF};
#endif
Deduplicator<bool> work_mode_low_power_;
};
} // namespace esphome::ld6002b
+6 -162
View File
@@ -1,170 +1,14 @@
import esphome.codegen as cg
from esphome.components import number
from esphome.components.button import DOMAIN as BUTTON_DOMAIN
from esphome.components.select import DOMAIN as SELECT_DOMAIN
import esphome.config_validation as cv
from esphome.const import (
CONF_AREA_ID,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_DURATION,
ENTITY_CATEGORY_CONFIG,
UNIT_METER,
UNIT_MILLISECOND,
UNIT_SECOND,
)
import esphome.final_validate as fv
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from .. import LD6002BComponent, validate_unique_settings
from ..const import (
CONF_APPLY_AREA,
CONF_AREA_CONFIG,
CONF_HOLD_DELAY,
CONF_LD6002B_ID,
CONF_LOW_POWER_SLEEP_TIME,
CONF_Z_MAX,
CONF_Z_MIN,
KEY_X_MAX,
KEY_X_MIN,
KEY_Y_MAX,
KEY_Y_MIN,
)
from .. import LD6002BComponent
from ..const import CONF_LD6002B_ID
DEPENDENCIES = ["ld6002b"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_HOLD_DELAY): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_SECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MAX): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_LOW_POWER_SLEEP_TIME): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_AREA_CONFIG): cv.Schema(
{
cv.Optional(KEY_X_MIN): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(KEY_X_MAX): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(KEY_Y_MIN): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(KEY_Y_MAX): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MAX): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
),
}
)
def final_validate(config: ConfigType) -> None:
validate_unique_settings(number.DOMAIN, config)
if config.get(CONF_AREA_CONFIG) is None:
return
full_config = fv.full_config.get()
hub_id = config[CONF_LD6002B_ID]
has_apply_area = any(
entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_APPLY_AREA) is not None
for entry in full_config.get(BUTTON_DOMAIN, [])
)
if not has_apply_area:
raise cv.Invalid(
f"{CONF_AREA_CONFIG} requires button.apply_area for the same ld6002b instance",
path=[CONF_AREA_CONFIG],
)
has_area_id_select = any(
entry.get(CONF_LD6002B_ID) == hub_id and entry.get(CONF_AREA_ID) is not None
for entry in full_config.get(SELECT_DOMAIN, [])
)
if not has_area_id_select:
raise cv.Invalid(
f"{CONF_AREA_CONFIG} requires select.area_id for the same ld6002b instance",
path=[CONF_AREA_CONFIG],
)
FINAL_VALIDATE_SCHEMA = final_validate
CONFIG_SCHEMA = entities.number_schema(LD6002BComponent, CONF_LD6002B_ID)
FINAL_VALIDATE_SCHEMA = entities.number_final_validate(CONF_LD6002B_ID, "ld6002b")
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
control = {"apply": "set_number_value(entity, value)", "publish": "before"}
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_HOLD_DELAY,
hub.set_hold_delay_number,
min_value=0,
max_value=65535,
step=1,
**control,
)
meters = {"min_value": -10, "max_value": 10, "step": 0.1, **control}
await numbers(CONF_Z_MIN, hub.set_z_min_number, **meters)
await numbers(CONF_Z_MAX, hub.set_z_max_number, **meters)
# 0x0205 is a uint32 of ms with no documented maximum; cap it at a minute
await numbers(
CONF_LOW_POWER_SLEEP_TIME,
hub.set_low_power_sleep_number,
min_value=0,
max_value=60000,
step=100,
**control,
)
if area_config := config.get(CONF_AREA_CONFIG):
area = number.sub_numbers(area_config, parent=hub)
await area(KEY_X_MIN, hub.set_area_x_min_number, **meters)
await area(KEY_X_MAX, hub.set_area_x_max_number, **meters)
await area(KEY_Y_MIN, hub.set_area_y_min_number, **meters)
await area(KEY_Y_MAX, hub.set_area_y_max_number, **meters)
await area(CONF_Z_MIN, hub.set_area_z_min_number, **meters)
await area(CONF_Z_MAX, hub.set_area_z_max_number, **meters)
await entities.number_to_code(config, CONF_LD6002B_ID)
+7 -61
View File
@@ -1,71 +1,17 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_AREA_ID, CONF_SENSITIVITY, ENTITY_CATEGORY_CONFIG
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from .. import LD6002BComponent, validate_unique_settings
from ..const import CONF_INSTALLATION_MODE, CONF_LD6002B_ID, CONF_TRIGGER_SPEED
from .. import LD6002BComponent
from ..const import AREA_KINDS, CONF_LD6002B_ID
DEPENDENCIES = ["ld6002b"]
AREA_ID_OPTIONS = [
"interference_area_0",
"interference_area_1",
"interference_area_2",
"interference_area_3",
"detection_area_0",
"detection_area_1",
"detection_area_2",
"detection_area_3",
]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_SENSITIVITY): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_TRIGGER_SPEED): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_INSTALLATION_MODE): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_AREA_ID): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
}
CONFIG_SCHEMA = entities.select_schema(LD6002BComponent, CONF_LD6002B_ID)
FINAL_VALIDATE_SCHEMA = entities.unique_settings_final_validate(
select.DOMAIN, CONF_LD6002B_ID, "ld6002b"
)
FINAL_VALIDATE_SCHEMA = partial(validate_unique_settings, select.DOMAIN)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
control = {"apply": "set_select_value(entity, index)", "publish": "before"}
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_SENSITIVITY,
hub.set_sensitivity_select,
options=["low", "medium", "high"],
**control,
)
await selects(
CONF_TRIGGER_SPEED,
hub.set_trigger_speed_select,
options=["slow", "medium", "fast"],
**control,
)
await selects(
CONF_INSTALLATION_MODE,
hub.set_installation_select,
options=["top", "side"],
**control,
)
await selects(
CONF_AREA_ID, hub.set_area_id_select, options=AREA_ID_OPTIONS, **control
)
await entities.select_to_code(config, CONF_LD6002B_ID, area_kinds=AREA_KINDS)
+5 -163
View File
@@ -1,173 +1,15 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_TARGET_COUNT
import esphome.config_validation as cv
from esphome.const import (
CONF_X,
CONF_Y,
DEVICE_CLASS_DISTANCE,
STATE_CLASS_MEASUREMENT,
UNIT_METER,
)
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from . import LD6002BComponent
from .const import (
AREA_COUNT,
CONF_CLUSTER_ID,
CONF_DOPPLER_INDEX,
CONF_LD6002B_ID,
CONF_POINT_COUNT,
CONF_Z,
CONF_Z_MAX,
CONF_Z_MIN,
KEY_X_MAX,
KEY_X_MIN,
KEY_Y_MAX,
KEY_Y_MIN,
MAX_TARGETS,
)
from .const import AREA_KINDS, CONF_LD6002B_ID
DEPENDENCIES = ["ld6002b"]
TARGET_SCHEMA = cv.Schema(
{
cv.Optional(CONF_X): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Y): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Z): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_DOPPLER_INDEX): sensor.sensor_schema(
accuracy_decimals=0,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CLUSTER_ID): sensor.sensor_schema(
accuracy_decimals=0,
),
}
)
AREA_SCHEMA = cv.Schema(
{
cv.Optional(KEY_X_MIN): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(KEY_X_MAX): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(KEY_Y_MIN): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(KEY_Y_MAX): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Z_MIN): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_Z_MAX): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
# (config key, C++ setter axis) for the six bounds every area sensor block carries.
_AREA_AXES = (
(KEY_X_MIN, "x_min"),
(KEY_X_MAX, "x_max"),
(KEY_Y_MIN, "y_min"),
(KEY_Y_MAX, "y_max"),
(CONF_Z_MIN, "z_min"),
(CONF_Z_MAX, "z_max"),
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POINT_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)})
.extend(
{cv.Optional(f"interference_area_{i}"): AREA_SCHEMA for i in range(AREA_COUNT)}
)
.extend(
{cv.Optional(f"detection_area_{i}"): AREA_SCHEMA for i in range(AREA_COUNT)}
)
CONFIG_SCHEMA = entities.sensor_schema(
LD6002BComponent, CONF_LD6002B_ID, area_kinds=AREA_KINDS
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TARGET_COUNT, hub.set_target_count_sensor)
await sensors(CONF_POINT_COUNT, hub.set_point_count_sensor)
for i in range(MAX_TARGETS):
if target_config := config.get(f"target_{i + 1}"):
if x_config := target_config.get(CONF_X):
sens = await sensor.new_sensor(x_config)
cg.add(hub.set_target_x_sensor(i, sens))
if y_config := target_config.get(CONF_Y):
sens = await sensor.new_sensor(y_config)
cg.add(hub.set_target_y_sensor(i, sens))
if z_config := target_config.get(CONF_Z):
sens = await sensor.new_sensor(z_config)
cg.add(hub.set_target_z_sensor(i, sens))
if doppler_index_config := target_config.get(CONF_DOPPLER_INDEX):
sens = await sensor.new_sensor(doppler_index_config)
cg.add(hub.set_target_dop_idx_sensor(i, sens))
if cluster_id_config := target_config.get(CONF_CLUSTER_ID):
sens = await sensor.new_sensor(cluster_id_config)
cg.add(hub.set_target_cluster_id_sensor(i, sens))
for kind in ("interference", "detection"):
for i in range(AREA_COUNT):
if area_config := config.get(f"{kind}_area_{i}"):
for key, axis in _AREA_AXES:
if axis_config := area_config.get(key):
sens = await sensor.new_sensor(axis_config)
cg.add(getattr(hub, f"set_{kind}_area_{axis}_sensor")(i, sens))
await entities.sensor_to_code(config, CONF_LD6002B_ID, area_kinds=AREA_KINDS)
+12 -44
View File
@@ -1,58 +1,26 @@
from functools import partial
import esphome.codegen as cg
from esphome.components import switch
from esphome.components.ld600x import entities
import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from .. import LD6002BComponent, validate_unique_settings
from ..const import (
CONF_LD6002B_ID,
CONF_LOW_POWER,
CONF_POINT_CLOUD,
CONF_TARGET_DISPLAY,
)
from .. import LD6002BComponent
from ..const import CONF_LD6002B_ID, CONF_LOW_POWER
DEPENDENCIES = ["ld6002b"]
# None of these three carry an inversion. They name what the module is doing, not
# how something is wired to it, so an inverted one would only report the opposite
# of the truth -- and the boot restore, which applies a state nothing reports back,
# is where that would be hardest to spot.
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_LOW_POWER): switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_POINT_CLOUD): switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_TARGET_DISPLAY): switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="RESTORE_DEFAULT_ON",
),
}
CONFIG_SCHEMA = entities.switch_schema(
LD6002BComponent,
CONF_LD6002B_ID,
extra={cv.Optional(CONF_LOW_POWER): entities.CONFIG_SWITCH_SCHEMA},
)
FINAL_VALIDATE_SCHEMA = entities.unique_settings_final_validate(
switch.DOMAIN, CONF_LD6002B_ID, "ld6002b"
)
FINAL_VALIDATE_SCHEMA = partial(validate_unique_settings, switch.DOMAIN)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
control = {"apply": "set_switch_state(entity, state)", "publish": "after"}
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_LOW_POWER, hub.set_low_power_switch, **control)
await switches(CONF_POINT_CLOUD, hub.set_point_cloud_switch, **control)
await switches(CONF_TARGET_DISPLAY, hub.set_target_display_switch, **control)
await switches(CONF_LOW_POWER, hub.set_low_power_switch, **entities.SWITCH_CONTROL)
await entities.switch_to_code(config, CONF_LD6002B_ID)
+4 -20
View File
@@ -1,29 +1,13 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC
from esphome.components.ld600x import entities
from esphome.types import ConfigType
from . import LD6002BComponent
from .const import CONF_LD6002B_ID, CONF_OTA_VERSION, CONF_WORK_MODE
from .const import CONF_LD6002B_ID
DEPENDENCIES = ["ld6002b"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD6002B_ID): cv.use_id(LD6002BComponent),
cv.Optional(CONF_WORK_MODE): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_OTA_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
)
CONFIG_SCHEMA = entities.text_sensor_schema(LD6002BComponent, CONF_LD6002B_ID)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD6002B_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_WORK_MODE, hub.set_work_mode_text_sensor)
await text_sensors(CONF_OTA_VERSION, hub.set_ota_version_text_sensor)
await entities.text_sensor_to_code(config, CONF_LD6002B_ID)
+72
View File
@@ -0,0 +1,72 @@
"""Shared base for the Hi-Link 60 GHz radars that speak the TinyFrame serial protocol.
Covers the LD6002B and the LD6004. The LD6001A is a different family (AT commands and
another report format) and gets its own component, so it does not build on this one.
"""
from dataclasses import dataclass, field
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.cpp_generator import MockObj, MockObjClass
from .const import MAX_TARGETS
CODEOWNERS = ["@hepter", "@wolph"]
DEPENDENCIES = ["uart"]
DOMAIN = "ld600x"
ld600x_ns = cg.esphome_ns.namespace("ld600x")
LD600XComponent = ld600x_ns.class_("LD600XComponent", cg.Component, uart.UARTDevice)
AreaKind = ld600x_ns.enum("AreaKind")
AreaAxis = ld600x_ns.enum("AreaAxis")
ButtonType = ld600x_ns.enum("ButtonType")
_request_target_slot = cg.slot_counter("LD600X_MAX_TARGETS")
_request_area_kind_slot = cg.slot_counter("LD600X_AREA_KINDS")
@dataclass
class LD600XData:
# Area kinds already requested per hub, so repeated requests keep the largest count.
area_kinds: dict[str, int] = field(default_factory=dict)
def _get_data() -> LD600XData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = LD600XData()
return CORE.data[DOMAIN]
def request_model_sizes(hub: MockObj, *, area_kinds: int) -> None:
"""Size the hub's target and area arrays; call it again to raise the area kinds."""
requested = _get_data().area_kinds
key = str(hub)
if key not in requested:
for _ in range(MAX_TARGETS):
_request_target_slot(key)
for _ in range(requested.get(key, 0), area_kinds):
_request_area_kind_slot(key)
requested[key] = max(requested.get(key, 0), area_kinds)
def ld600x_hub_schema(component_class: MockObjClass) -> cv.Schema:
return (
cv.Schema({cv.GenerateID(): cv.declare_id(component_class)})
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
def uart_final_validate(component_name: str) -> cv.Schema:
return uart.final_validate_device_schema(
component_name,
baud_rate=115200,
require_tx=True,
require_rx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
+34
View File
@@ -0,0 +1,34 @@
AREA_COUNT = 4
CONF_APPLY_AREA = "apply_area"
CONF_AREA_CONFIG = "area_config"
CONF_AUTO_INTERFERENCE = "auto_interference"
CONF_CLEAR_INTERFERENCE = "clear_interference"
CONF_CLUSTER_ID = "cluster_id"
CONF_DOPPLER_INDEX = "doppler_index"
CONF_GET_AREAS = "get_areas"
CONF_GET_DELAY = "get_delay"
CONF_GET_INSTALLATION = "get_installation"
CONF_GET_LOW_POWER_MODE = "get_low_power_mode"
CONF_GET_LOW_POWER_SLEEP_TIME = "get_low_power_sleep_time"
CONF_GET_SENSITIVITY = "get_sensitivity"
CONF_GET_TRIGGER_SPEED = "get_trigger_speed"
CONF_GET_Z_RANGE = "get_z_range"
CONF_HOLD_DELAY = "hold_delay"
CONF_INSTALLATION_MODE = "installation_mode"
CONF_LOW_POWER_SLEEP_TIME = "low_power_sleep_time"
CONF_OTA_VERSION = "ota_version"
CONF_POINT_CLOUD = "point_cloud"
CONF_POINT_COUNT = "point_count"
CONF_RESET_DETECTION_AREA = "reset_detection_area"
CONF_RESET_UNATTENDED = "reset_unattended"
CONF_TARGET_DISPLAY = "target_display"
CONF_TRIGGER_SPEED = "trigger_speed"
CONF_WORK_MODE = "work_mode"
CONF_Z = "z"
CONF_Z_MAX = "z_max"
CONF_Z_MIN = "z_min"
KEY_X_MAX = "x_max"
KEY_X_MIN = "x_min"
KEY_Y_MAX = "y_max"
KEY_Y_MIN = "y_min"
MAX_TARGETS = 3
+640
View File
@@ -0,0 +1,640 @@
from collections.abc import Callable
from typing import Any
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import (
binary_sensor,
button,
number,
select,
sensor,
switch,
text_sensor,
)
from esphome.components.const import CONF_TARGET_COUNT
import esphome.config_validation as cv
from esphome.const import (
CONF_AREA_ID,
CONF_ID,
CONF_PLATFORM,
CONF_SENSITIVITY,
CONF_TARGET,
CONF_X,
CONF_Y,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_DURATION,
DEVICE_CLASS_OCCUPANCY,
DEVICE_CLASS_SWITCH,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
UNIT_METER,
UNIT_MILLISECOND,
UNIT_SECOND,
)
from esphome.core import ID
from esphome.core.entity_helpers import SubEntities
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 .const import (
AREA_COUNT,
CONF_APPLY_AREA,
CONF_AREA_CONFIG,
CONF_AUTO_INTERFERENCE,
CONF_CLEAR_INTERFERENCE,
CONF_CLUSTER_ID,
CONF_DOPPLER_INDEX,
CONF_GET_AREAS,
CONF_GET_DELAY,
CONF_GET_INSTALLATION,
CONF_GET_LOW_POWER_MODE,
CONF_GET_LOW_POWER_SLEEP_TIME,
CONF_GET_SENSITIVITY,
CONF_GET_TRIGGER_SPEED,
CONF_GET_Z_RANGE,
CONF_HOLD_DELAY,
CONF_INSTALLATION_MODE,
CONF_LOW_POWER_SLEEP_TIME,
CONF_OTA_VERSION,
CONF_POINT_CLOUD,
CONF_POINT_COUNT,
CONF_RESET_DETECTION_AREA,
CONF_RESET_UNATTENDED,
CONF_TARGET_DISPLAY,
CONF_TRIGGER_SPEED,
CONF_WORK_MODE,
CONF_Z,
CONF_Z_MAX,
CONF_Z_MIN,
KEY_X_MAX,
KEY_X_MIN,
KEY_Y_MAX,
KEY_Y_MIN,
MAX_TARGETS,
)
_AREA_KINDS = {
"interference": AreaKind.AREA_KIND_INTERFERENCE,
"detection": AreaKind.AREA_KIND_DETECTION,
"dwell": AreaKind.AREA_KIND_DWELL,
}
_AREA_AXES = (
(KEY_X_MIN, AreaAxis.AREA_AXIS_X_MIN),
(KEY_X_MAX, AreaAxis.AREA_AXIS_X_MAX),
(KEY_Y_MIN, AreaAxis.AREA_AXIS_Y_MIN),
(KEY_Y_MAX, AreaAxis.AREA_AXIS_Y_MAX),
(CONF_Z_MIN, AreaAxis.AREA_AXIS_Z_MIN),
(CONF_Z_MAX, AreaAxis.AREA_AXIS_Z_MAX),
)
_METER_SENSOR_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
_TARGET_AXIS_SCHEMA = sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
)
_METER_NUMBER_SCHEMA = number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_METER,
device_class=DEVICE_CLASS_DISTANCE,
entity_category=ENTITY_CATEGORY_CONFIG,
)
TARGET_SCHEMA = cv.Schema(
{
**{cv.Optional(key): _TARGET_AXIS_SCHEMA for key in (CONF_X, CONF_Y, CONF_Z)},
cv.Optional(CONF_DOPPLER_INDEX): sensor.sensor_schema(
accuracy_decimals=0,
filters=sensor.TIMEOUT_THROTTLE_FILTERS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CLUSTER_ID): sensor.sensor_schema(
accuracy_decimals=0,
),
}
)
AREA_SCHEMA = cv.Schema(
{cv.Optional(key): _METER_SENSOR_SCHEMA for key, _ in _AREA_AXES}
)
_TARGET_KEYS = tuple(f"target_{i + 1}" for i in range(MAX_TARGETS))
_DETECTION_AREA_KEYS = tuple(f"detection_area_{i}" for i in range(AREA_COUNT))
def _hub_has(domain: str, key: str, hub_id_key: str, hub_id: ID) -> bool:
"""Return whether a block of domain on the hub hub_id sets key."""
return any(
entry.get(hub_id_key) == hub_id and entry.get(key) is not None
for entry in fv.full_config.get().get(domain, [])
)
def sensor_schema(
hub_class: MockObjClass, hub_id_key: str, *, area_kinds: tuple[str, ...]
) -> cv.Schema:
return (
cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POINT_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend({cv.Optional(key): TARGET_SCHEMA for key in _TARGET_KEYS})
.extend(
{
cv.Optional(f"{kind}_area_{i}"): AREA_SCHEMA
for kind in area_kinds
for i in range(AREA_COUNT)
}
)
)
async def sensor_to_code(
config: ConfigType, hub_id_key: str, *, area_kinds: tuple[str, ...]
) -> None:
hub = await cg.get_variable(config[hub_id_key])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TARGET_COUNT, hub.set_target_count_sensor)
await sensors(CONF_POINT_COUNT, hub.set_point_count_sensor)
target_setters = (
(CONF_X, hub.set_target_x_sensor),
(CONF_Y, hub.set_target_y_sensor),
(CONF_Z, hub.set_target_z_sensor),
(CONF_DOPPLER_INDEX, hub.set_target_dop_idx_sensor),
(CONF_CLUSTER_ID, hub.set_target_cluster_id_sensor),
)
for i, target_key in enumerate(_TARGET_KEYS):
if target_config := config.get(target_key):
for key, setter in target_setters:
if conf := target_config.get(key):
cg.add(setter(i, await sensor.new_sensor(conf)))
for kind in area_kinds:
area_kind = _AREA_KINDS[kind]
for i in range(AREA_COUNT):
if area_config := config.get(f"{kind}_area_{i}"):
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))
_OCCUPANCY_SCHEMA = binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY
)
def binary_sensor_schema(hub_class: MockObjClass, hub_id_key: str) -> cv.Schema:
return cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
cv.Optional(CONF_TARGET): _OCCUPANCY_SCHEMA,
**{
cv.Optional(key): _OCCUPANCY_SCHEMA
for key in (*_TARGET_KEYS, *_DETECTION_AREA_KEYS)
},
}
)
async def binary_sensor_to_code(config: ConfigType, hub_id_key: str) -> None:
hub = await cg.get_variable(config[hub_id_key])
sensors = binary_sensor.sub_binary_sensors(config)
await sensors(CONF_TARGET, hub.set_presence_binary_sensor)
for keys, setter in (
(_TARGET_KEYS, hub.set_target_presence_binary_sensor),
(_DETECTION_AREA_KEYS, hub.set_area_presence_binary_sensor),
):
for i, key in enumerate(keys):
if conf := config.get(key):
cg.add(setter(i, await binary_sensor.new_binary_sensor(conf)))
def text_sensor_schema(hub_class: MockObjClass, hub_id_key: str) -> cv.Schema:
return cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
cv.Optional(CONF_WORK_MODE): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Optional(CONF_OTA_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
)
async def text_sensor_to_code(config: ConfigType, hub_id_key: str) -> None:
hub = await cg.get_variable(config[hub_id_key])
sensors = text_sensor.sub_text_sensors(config)
await sensors(CONF_WORK_MODE, hub.set_work_mode_text_sensor)
await sensors(CONF_OTA_VERSION, hub.set_ota_version_text_sensor)
def number_schema(
hub_class: MockObjClass, hub_id_key: str, *, extra: dict[Any, Any] | None = None
) -> cv.Schema:
return (
cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
cv.Optional(CONF_HOLD_DELAY): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_SECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_Z_MIN): _METER_NUMBER_SCHEMA,
cv.Optional(CONF_Z_MAX): _METER_NUMBER_SCHEMA,
cv.Optional(CONF_LOW_POWER_SLEEP_TIME): number.number_schema(
number.ApplyNumber,
unit_of_measurement=UNIT_MILLISECOND,
device_class=DEVICE_CLASS_DURATION,
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_AREA_CONFIG): cv.Schema(
{cv.Optional(key): _METER_NUMBER_SCHEMA for key, _ in _AREA_AXES}
),
}
)
).extend(extra or {})
def number_final_validate(
hub_id_key: str, component_name: str
) -> Callable[[ConfigType], None]:
unique = unique_settings_final_validate(number.DOMAIN, hub_id_key, component_name)
def final_validate(config: ConfigType) -> None:
unique(config)
if config.get(CONF_AREA_CONFIG) is None:
return
hub_id: ID = config[hub_id_key]
if not _hub_has(button.DOMAIN, CONF_APPLY_AREA, hub_id_key, hub_id):
raise cv.Invalid(
f"{CONF_AREA_CONFIG} requires button.apply_area for the same {component_name} instance",
path=[CONF_AREA_CONFIG],
)
if not _hub_has(select.DOMAIN, CONF_AREA_ID, hub_id_key, hub_id):
raise cv.Invalid(
f"{CONF_AREA_CONFIG} requires select.area_id for the same {component_name} instance",
path=[CONF_AREA_CONFIG],
)
return final_validate
# The apply call picks the setting by entity pointer, so every number shares one generated class.
NUMBER_CONTROL: dict[str, str] = {
"apply": "set_number_value(entity, value)",
"publish": "before",
}
async def number_to_code(config: ConfigType, hub_id_key: str) -> SubEntities:
"""Create the shared numbers and return the bound helper for a model's own numbers."""
hub = await cg.get_variable(config[hub_id_key])
numbers = number.sub_numbers(config, parent=hub)
await numbers(
CONF_HOLD_DELAY,
hub.set_hold_delay_number,
min_value=0,
max_value=65535,
step=1,
**NUMBER_CONTROL,
)
meters: dict[str, Any] = {
"min_value": -10,
"max_value": 10,
"step": 0.1,
**NUMBER_CONTROL,
}
await numbers(CONF_Z_MIN, hub.set_z_min_number, **meters)
await numbers(CONF_Z_MAX, hub.set_z_max_number, **meters)
# 0x0205 is a uint32 of ms with no documented maximum; cap it at a minute
await numbers(
CONF_LOW_POWER_SLEEP_TIME,
hub.set_low_power_sleep_number,
min_value=0,
max_value=60000,
step=100,
**NUMBER_CONTROL,
)
if area_config := config.get(CONF_AREA_CONFIG):
area = number.sub_numbers(area_config, parent=hub)
await area(KEY_X_MIN, hub.set_area_x_min_number, **meters)
await area(KEY_X_MAX, hub.set_area_x_max_number, **meters)
await area(KEY_Y_MIN, hub.set_area_y_min_number, **meters)
await area(KEY_Y_MAX, hub.set_area_y_max_number, **meters)
await area(CONF_Z_MIN, hub.set_area_z_min_number, **meters)
await area(CONF_Z_MAX, hub.set_area_z_max_number, **meters)
return numbers
def select_schema(
hub_class: MockObjClass, hub_id_key: str, *, extra: dict[Any, Any] | None = None
) -> cv.Schema:
return (
cv.Schema(
{
cv.GenerateID(hub_id_key): cv.use_id(hub_class),
cv.Optional(CONF_SENSITIVITY): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_TRIGGER_SPEED): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_INSTALLATION_MODE): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
cv.Optional(CONF_AREA_ID): select.select_schema(
select.ApplySelect, entity_category=ENTITY_CATEGORY_CONFIG
),
}
)
).extend(extra or {})
SELECT_CONTROL: dict[str, str] = {
"apply": "set_select_value(entity, index)",
"publish": "before",
}
async def select_to_code(
config: ConfigType, hub_id_key: str, *, area_kinds: tuple[str, ...]
) -> SubEntities:
"""Create the shared selects and return the bound helper for a model's own selects."""
hub = await cg.get_variable(config[hub_id_key])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_SENSITIVITY,
hub.set_sensitivity_select,
options=["low", "medium", "high"],
**SELECT_CONTROL,
)
await selects(
CONF_TRIGGER_SPEED,
hub.set_trigger_speed_select,
options=["slow", "medium", "fast"],
**SELECT_CONTROL,
)
await selects(
CONF_INSTALLATION_MODE,
hub.set_installation_select,
options=["top", "side"],
**SELECT_CONTROL,
)
await selects(
CONF_AREA_ID,
hub.set_area_id_select,
options=area_id_options(area_kinds),
**SELECT_CONTROL,
)
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(
switch.ApplySwitch,
block_inverted=True,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="RESTORE_DEFAULT_ON",
),
}
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
},
}
).extend(extra or {})
SWITCH_CONTROL: dict[str, str] = {
"apply": "set_switch_state(entity, state)",
"publish": "after",
}
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(
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_GET_AREAS: DIAGNOSTIC_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,
CONF_GET_Z_RANGE: DIAGNOSTIC_BUTTON_SCHEMA,
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,
}
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
},
}
).extend(extra or {})
def button_final_validate(
hub_id_key: str, component_name: str
) -> Callable[[ConfigType], None]:
def final_validate(config: ConfigType) -> None:
hub_id: ID = config[hub_id_key]
if config.get(CONF_APPLY_AREA) and not _hub_has(
select.DOMAIN, CONF_AREA_ID, hub_id_key, hub_id
):
raise cv.Invalid(
f"{CONF_APPLY_AREA} requires select.area_id for the same {component_name} instance",
path=[CONF_APPLY_AREA],
)
return final_validate
BUTTON_MAP = {
CONF_APPLY_AREA: ButtonType.BUTTON_APPLY_AREA,
CONF_AUTO_INTERFERENCE: ButtonType.BUTTON_AUTO_INTERFERENCE,
CONF_GET_AREAS: ButtonType.BUTTON_GET_AREAS,
CONF_CLEAR_INTERFERENCE: ButtonType.BUTTON_CLEAR_INTERFERENCE,
CONF_RESET_DETECTION_AREA: ButtonType.BUTTON_RESET_DETECTION_AREA,
CONF_GET_DELAY: ButtonType.BUTTON_GET_DELAY,
CONF_GET_SENSITIVITY: ButtonType.BUTTON_GET_SENSITIVITY,
CONF_GET_TRIGGER_SPEED: ButtonType.BUTTON_GET_TRIGGER_SPEED,
CONF_GET_Z_RANGE: ButtonType.BUTTON_GET_Z_RANGE,
CONF_GET_INSTALLATION: ButtonType.BUTTON_GET_INSTALLATION,
CONF_GET_LOW_POWER_MODE: ButtonType.BUTTON_GET_LOW_POWER_MODE,
CONF_GET_LOW_POWER_SLEEP_TIME: ButtonType.BUTTON_GET_LOW_POWER_SLEEP_TIME,
CONF_RESET_UNATTENDED: ButtonType.BUTTON_RESET_UNATTENDED,
}
async def button_to_code(
config: ConfigType,
hub_id_key: str,
*,
extra: dict[str, MockObj] | None = None,
) -> None:
extra = extra or {}
confs: list[tuple[MockObj, ConfigType]] = [
(button_type, conf)
for key, button_type in {**BUTTON_MAP, **extra}.items()
if (conf := config.get(key)) is not None
]
if not confs:
return
if any(key in config for key in extra):
# Two enum types in one ?: chain draw a warning, so a model's buttons put everything in uint8_t.
confs = [
(cg.RawExpression(f"static_cast<uint8_t>({button_type})"), conf)
for button_type, conf in confs
]
hub = await cg.get_variable(config[hub_id_key])
params = button.APPLY_ENTITY.params
# One function for all buttons, picking the type by pointer: one class instead of one per button.
# Named after the first button so two button blocks on one hub do not clash.
fn = f"esphome__{confs[0][1][CONF_ID].id}__button_fn"
cg.add_global(
cg.RawStatement(f"static void {fn}({ParameterListExpression(*params)});")
)
template = cg.TemplateArguments(cg.RawExpression(fn))
created: list[tuple[MockObj, MockObj]] = [
(button_type, await button.new_button(conf, template))
for button_type, conf in confs
]
*head, (last_type, _) = created
chain = "".join(f"entity == {parent_ref(var)} ? {t} : " for t, var in head)
cg.static_function(
fn,
cg.void,
list(params),
[
f"const auto type = {chain}{last_type};",
f"{parent_ref(hub)}->press_button(type);",
],
)
def unique_settings_final_validate(
domain: str, hub_id_key: str, component_name: str
) -> Callable[[ConfigType], None]:
"""Reject a setting set in two platform blocks of one hub, which keeps one entity per setting."""
def final_validate(config: ConfigType) -> None:
hub_id: ID = config[hub_id_key]
entries: list[ConfigType] = [
entry
for entry in fv.full_config.get().get(domain, [])
if entry.get(CONF_PLATFORM) == component_name
and entry.get(hub_id_key) == hub_id
]
for key, value in config.items():
if key in (CONF_PLATFORM, hub_id_key):
continue
# area_config groups six settings, so each one is checked on its own
for path in (
[[key, sub] for sub in value] if key == CONF_AREA_CONFIG else [[key]]
):
if sum(_has_path(entry, path) for entry in entries) > 1:
raise cv.Invalid(
f"{'.'.join(path)} is already set for this {component_name} in another {domain} block",
path=path,
)
return final_validate
def _has_path(entry: ConfigType, path: list[str]) -> bool:
for key in path:
if key not in entry:
return False
entry = entry[key]
return True
def area_id_options(area_kinds: tuple[str, ...]) -> list[str]:
return [f"{kind}_area_{i}" for kind in area_kinds for i in range(AREA_COUNT)]
File diff suppressed because it is too large. Load diff
+376
View File
@@ -0,0 +1,376 @@
#pragma once
// Shared base for the Hi-Link 60 GHz radars that speak the TinyFrame serial protocol: the LD6002B and
// the LD6004. The LD6001A uses AT commands and another report format and is a separate component.
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/ld600x/ld600x_frame.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensor/sensor_with_dedup.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <array>
#include <cmath>
namespace esphome::ld600x {
static constexpr uint8_t AREA_COUNT = 4;
// Interference areas own ids 0..AREA_COUNT-1 and detection areas the next four, so
// this is the whole id space TYPE_SET_AREA accepts.
static constexpr uint8_t AREA_ID_COUNT = AREA_COUNT * LD600X_AREA_KINDS;
static constexpr uint16_t DEFAULT_MAX_DATA_LEN = 1024;
static constexpr uint16_t DEFAULT_MAX_DATA_LEN_POINT_CLOUD = 4096;
// Largest protocol payload is TYPE_SET_AREA: int32 area id + 6 floats = 28 bytes.
static constexpr size_t CMD_MAX_DATA_LEN = 28;
static constexpr uint8_t LD600X_MODEL_KIND_BASE = 0x80;
enum AreaKind : uint8_t { AREA_KIND_INTERFERENCE, AREA_KIND_DETECTION, AREA_KIND_DWELL };
enum AreaAxis : uint8_t {
AREA_AXIS_X_MIN,
AREA_AXIS_X_MAX,
AREA_AXIS_Y_MIN,
AREA_AXIS_Y_MAX,
AREA_AXIS_Z_MIN,
AREA_AXIS_Z_MAX,
AREA_AXIS_COUNT,
};
enum ButtonType : uint8_t {
BUTTON_APPLY_AREA,
BUTTON_AUTO_INTERFERENCE,
BUTTON_GET_AREAS,
BUTTON_CLEAR_INTERFERENCE,
BUTTON_RESET_DETECTION_AREA,
BUTTON_GET_DELAY,
BUTTON_GET_SENSITIVITY,
BUTTON_GET_TRIGGER_SPEED,
BUTTON_GET_Z_RANGE,
BUTTON_GET_INSTALLATION,
BUTTON_GET_LOW_POWER_MODE,
BUTTON_GET_LOW_POWER_SLEEP_TIME,
BUTTON_RESET_UNATTENDED,
};
#ifdef USE_SENSOR
struct TargetSensors {
sensor::Sensor *x{nullptr};
sensor::Sensor *y{nullptr};
sensor::Sensor *z{nullptr};
sensor::Sensor *dop_idx{nullptr};
// A cluster id names a person, so like the counts it is published on change, not per frame.
sensor::SensorWithDedup<int32_t> cluster_id;
};
// Indexed by AreaAxis.
using AreaSensors = std::array<sensor::Sensor *, AREA_AXIS_COUNT>;
#endif
// Index of one area in the tables that hold every area of every kind.
static constexpr uint8_t area_index(uint8_t kind, uint8_t area) { return kind * AREA_COUNT + area; }
struct AreaConfig {
float x_min{NAN};
float x_max{NAN};
float y_min{NAN};
float y_max{NAN};
float z_min{NAN};
float z_max{NAN};
};
struct VersionPref {
char value[20];
};
class LD600XComponent : public Component, public uart::UARTDevice {
public:
// The tag comes from LOG_STR, so it stays in flash on ESP8266 like an ESPHOME_LOG_TAG. It keeps the
// model's own name on every log line, so a logger `logs:` filter on `ld6002b` keeps working.
explicit LD600XComponent(const LogString *log_tag) : log_tag_(LOG_STR_ARG(log_tag)) {}
void setup() override;
void loop() override;
void dump_config() override;
#ifdef USE_SENSOR
SUB_SENSOR_WITH_DEDUP(target_count, uint32_t)
SUB_SENSOR_WITH_DEDUP(point_count, uint32_t)
void set_target_x_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= LD600X_MAX_TARGETS)
return;
this->targets_[target].x = sensor;
}
void set_target_y_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= LD600X_MAX_TARGETS)
return;
this->targets_[target].y = sensor;
}
void set_target_z_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= LD600X_MAX_TARGETS)
return;
this->targets_[target].z = sensor;
}
void set_target_dop_idx_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= LD600X_MAX_TARGETS)
return;
this->targets_[target].dop_idx = sensor;
}
void set_target_cluster_id_sensor(uint8_t target, sensor::Sensor *sensor) {
if (target >= LD600X_MAX_TARGETS)
return;
this->targets_[target].cluster_id.set_sensor(sensor);
}
void set_area_sensor(uint8_t kind, uint8_t area, uint8_t axis, sensor::Sensor *sensor) {
if (kind < LD600X_AREA_KINDS && area < AREA_COUNT && axis < AREA_AXIS_COUNT)
this->areas_[area_index(kind, area)][axis] = sensor;
}
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(presence)
void set_target_presence_binary_sensor(uint8_t target, binary_sensor::BinarySensor *sensor) {
if (target >= LD600X_MAX_TARGETS)
return;
this->target_presence_[target] = sensor;
}
void set_area_presence_binary_sensor(uint8_t area, binary_sensor::BinarySensor *sensor) {
if (area >= AREA_COUNT)
return;
this->area_presence_[area] = sensor;
}
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(work_mode)
SUB_TEXT_SENSOR(ota_version)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(hold_delay)
SUB_NUMBER(z_min)
SUB_NUMBER(z_max)
SUB_NUMBER(low_power_sleep)
SUB_NUMBER(area_x_min)
SUB_NUMBER(area_x_max)
SUB_NUMBER(area_y_min)
SUB_NUMBER(area_y_max)
SUB_NUMBER(area_z_min)
SUB_NUMBER(area_z_max)
#endif
#ifdef USE_SELECT
SUB_SELECT(sensitivity)
SUB_SELECT(trigger_speed)
SUB_SELECT(installation)
SUB_SELECT(area_id)
#endif
#ifdef USE_SWITCH
SUB_SWITCH(point_cloud)
SUB_SWITCH(target_display)
#endif
// Each setter picks its setting by entity pointer, so the entities share one generated class. A model
// with its own entities handles those first and passes the rest down.
#ifdef USE_NUMBER
virtual void set_number_value(number::Number *entity, float value);
#endif
#ifdef USE_SELECT
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);
#endif
#ifdef USE_BUTTON
virtual void press_button(uint8_t kind);
#endif
protected:
virtual bool handle_model_report(uint16_t type, const uint8_t *data, uint16_t len) { return false; }
// Whether one of the model's own entities reads the target stream, so boot switches it on.
virtual bool model_wants_target_stream() const { return false; }
// Called after every target or area presence report and whenever target presence is cleared.
virtual void on_presence_update() {}
// Lets the model hold a frame back (for example to wake the module first) and write it later with
// write_frame_(). Returns true when it took the frame.
virtual bool defer_frame_write(uint16_t type, const uint8_t *data, uint8_t len, bool track) { return false; }
virtual void setup_model() {}
virtual void dump_model_config() {}
const char *log_tag_;
struct PendingCommand {
uint16_t type{0};
uint8_t len{0};
std::array<uint8_t, CMD_MAX_DATA_LEN> data{};
};
void handle_frame_(uint16_t type, const uint8_t *data, uint16_t len);
void handle_target_report_(const uint8_t *data, uint16_t len);
void handle_point_cloud_(const uint8_t *data, uint16_t len);
void handle_area_presence_(const uint8_t *data, uint16_t len);
void handle_area_report_(uint8_t kind, const uint8_t *data, uint16_t len);
void handle_delay_report_(const uint8_t *data, uint16_t len);
void handle_sensitivity_report_(const uint8_t *data, uint16_t len);
void handle_trigger_speed_report_(const uint8_t *data, uint16_t len);
void handle_z_range_report_(const uint8_t *data, uint16_t len);
void handle_installation_report_(const uint8_t *data, uint16_t len);
void handle_low_power_sleep_report_(const uint8_t *data, uint16_t len);
void handle_version_report_(const uint8_t *data, uint16_t len);
// Drops every target-derived reading and the slot table they are indexed by.
void clear_target_state_();
void clear_area_presence_();
void restore_deferred_edits_();
void publish_area_numbers_();
#ifdef USE_SENSOR
void clear_target_slot_(uint8_t index);
#endif
#ifdef USE_NUMBER
void publish_number_clamped_(number::Number *number, float value);
#endif
void update_area_numbers_(const AreaConfig &area);
void update_area_numbers_for_id_(uint8_t area_id);
bool queue_area_config_(uint8_t area_id, const AreaConfig &desired);
void try_apply_pending_area_(uint8_t reported_kind);
void init_area_id_pref_();
void save_area_id_pref_(uint8_t value);
void init_version_pref_();
void save_version_pref_(const char *value);
// Returns whether the command was queued: it is dropped, with a log line, when
// the payload is too long or the ring is full.
bool queue_command_(uint16_t type, const uint8_t *data, uint8_t len);
void process_command_queue_();
void send_command_(uint16_t type, const uint8_t *data, uint8_t len);
void send_command_internal_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
void write_frame_(uint16_t type, const uint8_t *data, uint8_t len, bool track);
// Returns whether the command reached the queue; see queue_command_.
bool send_control_command_(uint32_t command);
void send_z_range_();
void apply_area_config_();
#ifdef USE_SENSOR
std::array<TargetSensors, LD600X_MAX_TARGETS> targets_{};
std::array<AreaSensors, AREA_COUNT * LD600X_AREA_KINDS> areas_{};
#endif
#ifdef USE_BINARY_SENSOR
std::array<binary_sensor::BinarySensor *, LD600X_MAX_TARGETS> target_presence_{};
std::array<binary_sensor::BinarySensor *, AREA_COUNT> area_presence_{};
#endif
#ifdef USE_TEXT_SENSOR
ESPPreferenceObject version_pref_{};
bool version_pref_initialized_{false};
#endif
#ifdef USE_SELECT
ESPPreferenceObject area_id_pref_{};
bool area_id_pref_initialized_{false};
#endif
FrameParser parser_;
uint16_t next_frame_id_{0};
// Sized for the two bursts that reach it, both counted as what is still queued
// once the first command is dequeued: boot leaves 11 with every platform
// configured, and pressing all fourteen buttons before an ack lands leaves 15.
// Neither overflowed 16, but one free slot is not headroom, and overflowing is a
// dropped command with only a log line to show for it. Costs 256 bytes more per
// configured instance, and this component is MULTI_CONF.
static constexpr uint8_t CMD_QUEUE_SIZE = 24;
static constexpr uint32_t CMD_ACK_TIMEOUT_MS = 300;
// A sleeping module consumes the first frame to wake and answers only the one after it.
static constexpr uint32_t CMD_FIRST_ACK_TIMEOUT_MS = 600;
// How long the module stays awake after any frame, and so still answers the next one.
static constexpr uint32_t MODULE_AWAKE_MS = 10000;
static constexpr uint8_t CMD_MAX_RETRIES = 3;
// Free for one keyed timer of the model's own.
static constexpr uint32_t MODEL_TIMEOUT_ID = 0;
// Keyed so a burst of writes collapses to one read once they settle, rather than
// one read per write.
static constexpr uint32_t AREA_REFRESH_TIMEOUT_ID = 1;
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
StaticRingBuffer<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_;
bool command_active_{false};
bool command_sent_{false};
PendingCommand active_command_{};
uint8_t retries_left_{0};
uint32_t last_send_ms_{0};
// Last frame seen in either direction; any traffic keeps the module awake.
uint32_t last_traffic_ms_{0};
// Frames transmitted for the command in flight, including retries; drives the retry budget.
uint8_t attempts_sent_{0};
// Subset of those the module can actually answer: a frame that woke it is consumed, not replied to.
uint8_t acks_expected_{0};
// ACKs still owed for superseded attempts; they carry no id, only their arrival order.
uint16_t stale_ack_type_{0};
uint8_t stale_ack_count_{0};
// When that debt was booked, so a debt no reply can still settle expires instead of eating a live ACK.
uint32_t stale_ack_ms_{0};
// Bumped whenever the active command changes, so a deferred send can tell it was retired.
uint8_t send_generation_{0};
float z_min_{NAN};
float z_max_{NAN};
float area_x_min_{NAN};
float area_x_max_{NAN};
float area_y_min_{NAN};
float area_y_max_{NAN};
float area_z_min_{NAN};
float area_z_max_{NAN};
// What the user has typed and not yet applied; NaN per axis means "nothing of
// mine here, take the module's value". Same sentinel shape as
// pending_area_updates_. Exactly two things empty it: the area_id select moving
// to another area, and an apply that was accepted. A write the bounds guard
// refused leaves it alone, and a deferred apply that had to be dropped hands its
// staged values back here -- but only while the user is still on the area they
// were staged for. Either way the values stay the user's to fix.
AreaConfig area_edits_{};
std::array<AreaConfig, AREA_COUNT * LD600X_AREA_KINDS> area_values_{};
uint8_t area_id_{0xFF};
bool area_id_set_{false};
// Which person owns each target_N slot, so a slot survives the module re-sorting its array.
std::array<int32_t, LD600X_MAX_TARGETS> slot_cluster_{};
std::array<bool, LD600X_MAX_TARGETS> slot_occupied_{};
bool target_presence_any_{false};
// What the switches and setup asked the module for, which is not the same as
// what it is doing yet: a stream keeps sending until it acts on the command.
// The report handlers read these and drop anything a stopped stream still emits.
bool target_display_enabled_{false};
bool point_cloud_enabled_{false};
bool area_presence_any_{false};
bool area_write_in_flight_{false};
bool deferred_apply_pending_{false};
uint8_t pending_area_id_{0xFF};
AreaConfig pending_area_updates_{};
#ifdef USE_SENSOR
std::array<bool, LD600X_MAX_TARGETS> last_target_presence_{}; // one-shot NAN clear for target sensors
#endif
};
} // namespace esphome::ld600x
+104
View File
@@ -0,0 +1,104 @@
#include "ld600x_frame.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome::ld600x {
ESPHOME_LOG_TAG(TAG, "ld600x");
void FrameParser::init(uint8_t *buffer, uint16_t capacity) {
this->data_buf_ = buffer;
this->max_data_len_ = capacity;
this->reset();
}
void FrameParser::start_header_() {
this->pos_ = 0;
this->xor_ = TF_SOF;
this->parse_state_ = ParseState::PARSE_STATE_HEADER;
}
bool FrameParser::feed(uint8_t byte) {
switch (this->parse_state_) {
case ParseState::PARSE_STATE_DISCARD:
// pos_ counts the payload down; the byte after it is the data checksum.
if (this->pos_ == 0) {
this->reset();
} else {
this->pos_--;
}
return false;
case ParseState::PARSE_STATE_SOF:
if (byte == TF_SOF)
this->start_header_();
return false;
case ParseState::PARSE_STATE_HEADER: {
// Big-endian id, length and type, shifted in as they arrive so the buffer only ever holds a payload
uint16_t &field = this->pos_ < 2 ? this->frame_id_ : this->pos_ < 4 ? this->data_len_ : this->frame_type_;
field = static_cast<uint16_t>((field << 8) | byte);
this->xor_ ^= byte;
if (++this->pos_ == TF_HEADER_LEN)
this->parse_state_ = ParseState::PARSE_STATE_HCK;
return false;
}
case ParseState::PARSE_STATE_DATA:
this->data_buf_[this->pos_++] = byte;
this->xor_ ^= byte;
if (this->pos_ >= this->data_len_) {
this->parse_state_ = ParseState::PARSE_STATE_DCK;
}
return false;
case ParseState::PARSE_STATE_HCK:
case ParseState::PARSE_STATE_DCK: {
const bool header = this->parse_state_ == ParseState::PARSE_STATE_HCK;
if (byte != static_cast<uint8_t>(~this->xor_)) {
ESP_LOGV(TAG, "%s checksum mismatch", header ? LOG_STR_LITERAL("Header") : LOG_STR_LITERAL("Data"));
// The byte may be the SOF of the next frame when the real checksum was lost on the wire.
if (byte == TF_SOF) {
this->start_header_();
} else {
this->reset();
}
return false;
}
if (!header || this->data_len_ == 0) {
this->reset();
return true;
}
if (this->data_len_ > this->max_data_len_) {
ESP_LOGW(TAG, "Frame too large: %u", static_cast<unsigned>(this->data_len_));
this->pos_ = this->data_len_;
this->parse_state_ = ParseState::PARSE_STATE_DISCARD;
return false;
}
this->pos_ = 0;
this->xor_ = 0;
this->parse_state_ = ParseState::PARSE_STATE_DATA;
return false;
}
}
return false;
}
size_t encode_frame(uint16_t frame_id, uint16_t type, const uint8_t *data, uint16_t len, uint8_t *out) {
out[0] = TF_SOF;
out[1] = frame_id >> 8;
out[2] = frame_id & 0xFF;
out[3] = len >> 8;
out[4] = len & 0xFF;
out[5] = type >> 8;
out[6] = type & 0xFF;
// TinyFrame checksums are the inverted XOR of the bytes they cover, SOF included for the header.
out[TF_HEADER_LEN + 1] = static_cast<uint8_t>(~xor_bytes(out, TF_HEADER_LEN + 1));
size_t pos = TF_HEADER_LEN + 2;
if (len > 0 && data != nullptr) {
std::memcpy(out + pos, data, len);
pos += len;
out[pos++] = static_cast<uint8_t>(~xor_bytes(data, len));
}
return pos;
}
} // namespace esphome::ld600x
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/core/helpers.h"
namespace esphome::ld600x {
static constexpr uint8_t TF_SOF = 0x01;
// ID, payload length and type, excluding SOF and the header checksum.
static constexpr uint8_t TF_HEADER_LEN = 6;
// SOF, header, header checksum and data checksum around a payload.
static constexpr uint8_t TF_FRAME_OVERHEAD = TF_HEADER_LEN + 3;
inline float read_f32_le(const uint8_t *data) { return bit_cast<float>(encode_value_le<uint32_t>(data)); }
inline void write_u32_le(uint8_t *data, uint32_t value) {
data[0] = value & 0xFF;
data[1] = (value >> 8) & 0xFF;
data[2] = (value >> 16) & 0xFF;
data[3] = (value >> 24) & 0xFF;
}
inline void write_f32_le(uint8_t *data, float value) { write_u32_le(data, bit_cast<uint32_t>(value)); }
class FrameParser final {
public:
// The caller owns the buffer; payloads longer than capacity are skipped.
void init(uint8_t *buffer, uint16_t capacity);
bool feed(uint8_t byte);
uint16_t type() const { return this->frame_type_; }
uint16_t id() const { return this->frame_id_; }
uint16_t length() const { return this->data_len_; }
const uint8_t *data() const { return this->data_len_ == 0 ? nullptr : this->data_buf_; }
uint16_t capacity() const { return this->max_data_len_; }
void reset() { this->parse_state_ = ParseState::PARSE_STATE_SOF; }
protected:
enum class ParseState : uint8_t {
PARSE_STATE_SOF,
PARSE_STATE_HEADER,
PARSE_STATE_HCK,
PARSE_STATE_DATA,
PARSE_STATE_DCK,
PARSE_STATE_DISCARD,
};
void start_header_();
uint8_t *data_buf_{nullptr};
uint16_t data_len_{0};
uint16_t frame_type_{0};
uint16_t frame_id_{0};
// Header or payload write position, or the payload bytes left to discard.
uint16_t pos_{0};
uint16_t max_data_len_{0};
ParseState parse_state_{ParseState::PARSE_STATE_SOF};
uint8_t xor_{0};
};
// Frames this side sends have the top id bit set; the low 15 bits count up.
inline uint16_t next_frame_id(uint16_t &counter) { return (counter++ & 0x7FFF) | 0x8000; }
// Out must hold TF_FRAME_OVERHEAD + len bytes. Empty payloads have no data checksum.
size_t encode_frame(uint16_t frame_id, uint16_t type, const uint8_t *data, uint16_t len, uint8_t *out);
} // namespace esphome::ld600x
+1 -1
View File
@@ -192,7 +192,7 @@ uint32_t MAX31856Sensor::read_register24_(uint8_t reg) {
const uint8_t lsb(this->read_byte());
ESP_LOGVV(TAG, "read_byte lsb=0x%02X", lsb);
this->disable();
const uint32_t value((msb << 16) | (mid << 8) | lsb);
const uint32_t value = encode_uint24(msb, mid, lsb);
ESP_LOGV(TAG, "read_register_24_ reg=0x%02X: value=0x%06" PRIX32, reg, value);
return value;
}
+2 -1
View File
@@ -1,6 +1,7 @@
#include "max31865.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cmath>
#include <cinttypes>
@@ -184,7 +185,7 @@ uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) {
const uint8_t msb(this->read_byte());
const uint8_t lsb(this->read_byte());
this->disable();
const uint16_t value((msb << 8) | lsb);
const uint16_t value = encode_uint16(msb, lsb);
ESP_LOGVV(TAG, "read_register_16_ 0x%02X: 0x%04X", reg, value);
return value;
}
+6 -3
View File
@@ -60,9 +60,12 @@ void MAX9611Component::update() {
ESP_LOGW(TAG, "Update failed");
return;
}
uint16_t csa_register = ((register_map_[CSA_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[CSA_DATA_BYTE_LSB_ADRR])) >> 4;
uint16_t rs_register = ((register_map_[RS_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[RS_DATA_BYTE_LSB_ADRR])) >> 4;
uint16_t t_register = ((register_map_[TEMP_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[TEMP_DATA_BYTE_LSB_ADRR])) >> 7;
uint16_t csa_register =
encode_uint16(this->register_map_[CSA_DATA_BYTE_MSB_ADRR], this->register_map_[CSA_DATA_BYTE_LSB_ADRR]) >> 4;
uint16_t rs_register =
encode_uint16(this->register_map_[RS_DATA_BYTE_MSB_ADRR], this->register_map_[RS_DATA_BYTE_LSB_ADRR]) >> 4;
uint16_t t_register =
encode_uint16(this->register_map_[TEMP_DATA_BYTE_MSB_ADRR], this->register_map_[TEMP_DATA_BYTE_LSB_ADRR]) >> 7;
float voltage = rs_register * VOUT_LSB;
float shunt_voltage = (csa_register * gain_to_lsb(gain_)) / MICRO_VOLTS_PER_VOLT;
float temp = sign_extend<9>(t_register) * TEMP_LSB;
+12 -7
View File
@@ -1,5 +1,7 @@
#include "mhz19.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
@@ -44,12 +46,15 @@ void MHZ19Component::setup() {
}
void MHZ19Component::update() {
uint32_t now_ms = millis();
uint32_t warmup_ms = this->warmup_seconds_ * 1000;
if (now_ms < warmup_ms) {
ESP_LOGW(TAG, "MHZ19 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000);
this->status_set_warning();
return;
if (!this->warmup_complete_) {
uint32_t now_ms = App.get_loop_component_start_time();
uint32_t warmup_ms = this->warmup_seconds_ * 1000;
if (now_ms < warmup_ms) {
ESP_LOGW(TAG, "MHZ19 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000);
this->status_set_warning();
return;
}
this->warmup_complete_ = true;
}
uint8_t response[MHZ19_RESPONSE_LENGTH];
@@ -71,7 +76,7 @@ void MHZ19Component::update() {
return;
}
const uint16_t ppm = (uint16_t(response[2]) << 8) | response[3];
const uint16_t ppm = encode_uint16(response[2], response[3]);
const int temp = int(response[4]) - 40;
const uint8_t status = response[5];
+4 -5
View File
@@ -6,13 +6,13 @@
namespace esphome::mhz19 {
enum MHZ19ABCLogic {
enum MHZ19ABCLogic : uint8_t {
MHZ19_ABC_NONE = 0,
MHZ19_ABC_ENABLED,
MHZ19_ABC_DISABLED,
};
enum MHZ19DetectionRange {
enum MHZ19DetectionRange : uint8_t {
MHZ19_DETECTION_RANGE_DEFAULT = 0,
MHZ19_DETECTION_RANGE_0_2000PPM,
MHZ19_DETECTION_RANGE_0_5000PPM,
@@ -40,11 +40,10 @@ class MHZ19Component final : public PollingComponent, public uart::UARTDevice {
protected:
bool mhz19_write_command_(const uint8_t *command, uint8_t *response);
MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE};
uint32_t warmup_seconds_;
MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE};
MHZ19DetectionRange detection_range_{MHZ19_DETECTION_RANGE_DEFAULT};
bool warmup_complete_{false};
};
} // namespace esphome::mhz19
+2 -2
View File
@@ -229,7 +229,7 @@ def map_sequence(value):
The format is a repeated sequence of [CMD, <data>] where <data> is s a sequence of bytes. The length is inferred
from the length of the sequence and should not be explicit.
A single integer can be provided where there are no data bytes, in which case it is treated as a command.
A delay can be inserted by specifying "- delay N" where N is in ms
A delay can be inserted by specifying "- delay Nms"; any time unit is accepted, from 1 to 255 ms
"""
if isinstance(value, str) and value.lower().startswith("delay "):
value = value.lower()[6:]
@@ -676,7 +676,7 @@ class DriverChip:
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
if add_reset:
# Matches the 1-255ms range map_sequence() already allows for a "delay N" entry.
# Matches the 1-255ms range map_sequence() already allows for a "delay Nms" entry.
reset_delay = self.get_default("reset_delay", 10)
if reset_delay < 1 or reset_delay > 255:
raise ValueError("reset_delay must be between 1 and 255ms")
+2 -1
View File
@@ -58,7 +58,8 @@ t_rgb = ST7701S(
spi_mode="MODE3",
cs_pin={"xl9535": None, "number": 3},
requires={"psram"},
de_pin=45,
de_pin={"number": 45, "ignore_strapping_warning": True},
enable_pin={"number": 46, "ignore_strapping_warning": True},
hsync_pin=47,
vsync_pin=41,
pclk_pin=42,
@@ -411,7 +411,7 @@ bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len)
}
bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
Property::Decoder decoder{std::span{payload, len}, this->property_context_, this->pending_updates_};
Property::Decoder<UpdateFlags> decoder{std::span{payload, len}, this->property_context_, this->pending_updates_};
switch (msg_type) {
case STATUS_MSG_SETTINGS:
if (!decoder.decode_settings(this->status_)) {
@@ -513,7 +513,7 @@ void MitsubishiCN105::set_wide_vane_mode(WideVaneMode wide_vane_mode) {
void MitsubishiCN105::apply_settings_() {
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{};
Property::Encoder encoder{payload.data(), this->property_context_, this->pending_updates_};
Property::Encoder<UpdateFlags> encoder{payload.data(), this->property_context_, this->pending_updates_};
if (this->pending_updates_.contains(PropertyId::POWER) || this->pending_updates_.contains(PropertyId::MODE)) {
this->status_changed_ |= this->hold_if_run_ended_(this->unsent_from_power_on_, this->unsent_from_mode_);
this->request_telemetry_refresh_();
+6 -1
View File
@@ -40,7 +40,12 @@ MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; }
void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// Nothing extra to add here
// Home Assistant sends and expects ISO "YYYY-MM-DD", while state and command payloads here are JSON
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_VALUE_TEMPLATE] =
ESPHOME_F("{{ '%04d-%02d-%02d' | format(value_json.year, value_json.month, value_json.day) }}");
root[MQTT_COMMAND_TEMPLATE] =
ESPHOME_F("{\"year\":{{ value.year }},\"month\":{{ value.month }},\"day\":{{ value.day }}}");
}
bool MQTTDateComponent::send_initial_state() {
if (this->date_->has_state()) {
+9 -1
View File
@@ -51,7 +51,15 @@ MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")
const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; }
void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// Nothing extra to add here
// Home Assistant requires timezone-aware ISO datetimes; the device works in local time, which is
// assumed to match the Home Assistant time zone
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_VALUE_TEMPLATE] =
ESPHOME_F("{{ ('%04d-%02d-%02dT%02d:%02d:%02d' | format(value_json.year, value_json.month, value_json.day, "
"value_json.hour, value_json.minute, value_json.second)) | as_datetime | as_local }}");
root[MQTT_COMMAND_TEMPLATE] =
ESPHOME_F("{% set v = value | as_local %}{\"year\":{{ v.year }},\"month\":{{ v.month }},\"day\":{{ v.day }},"
"\"hour\":{{ v.hour }},\"minute\":{{ v.minute }},\"second\":{{ v.second }}}");
}
bool MQTTDateTimeComponent::send_initial_state() {
if (this->datetime_->has_state()) {
+6 -1
View File
@@ -40,7 +40,12 @@ MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")
const EntityBase *MQTTTimeComponent::get_entity() const { return this->time_; }
void MQTTTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// Nothing extra to add here
// Home Assistant sends and expects ISO "HH:MM:SS", while state and command payloads here are JSON
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_VALUE_TEMPLATE] =
ESPHOME_F("{{ '%02d:%02d:%02d' | format(value_json.hour, value_json.minute, value_json.second) }}");
root[MQTT_COMMAND_TEMPLATE] =
ESPHOME_F("{\"hour\":{{ value.hour }},\"minute\":{{ value.minute }},\"second\":{{ value.second }}}");
}
bool MQTTTimeComponent::send_initial_state() {
if (this->time_->has_state()) {
@@ -25,10 +25,6 @@ void MT6701I2CComponent::setup() {
void MT6701I2CComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MT6701 (I2C):");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
return;
}
if (this->direction_ != UNSET_U8) {
ESP_LOGCONFIG(TAG, " Direction bit: %u", this->direction_);
}
+5 -6
View File
@@ -54,7 +54,6 @@ using ip4_addr_t = in_addr;
#ifdef USE_ZEPHYR
#include <zephyr/net/net_ip.h>
#include <zephyr/net/socket.h>
#include <zephyr/posix/arpa/inet.h>
#ifndef CONFIG_NET_IPV6
#error "Zephyr builds need CONFIG_NET_IPV6"
#endif
@@ -64,12 +63,12 @@ static inline int ipaddr_aton(const char *cp, ip_addr_t *addr) {
// Dual-stack: a plain IPv4 literal must still parse -- store it as a v4-mapped
// IPv6 address (::ffff:a.b.c.d), matching the 4-octet constructor below.
struct in_addr addr4;
if (inet_pton(AF_INET, cp, &addr4) == 1) {
if (net_addr_pton(AF_INET, cp, &addr4) == 0) {
net_ipv6_addr_create_v4_mapped(&addr4, addr);
return 1;
}
#endif /* CONFIG_NET_IPV4 */
return inet_pton(AF_INET6, cp, addr) == 1 ? 1 : 0;
return net_addr_pton(AF_INET6, cp, addr) == 0 ? 1 : 0;
}
#endif /* USE_ZEPHYR */
@@ -90,7 +89,7 @@ namespace esphome::network {
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
static constexpr size_t IP_ADDRESS_BUFFER_SIZE =
#if defined(USE_ZEPHYR)
// Mainline Zephyr's inet_ntop() (net_addr_ntop() in subsys/net/ip/utils.c) never checks
// Mainline Zephyr's net_addr_ntop() (subsys/net/ip/utils.c) never checks
// its size argument, so an undersized buffer overflows instead of failing cleanly. Size
// for the worst case: an IPv4-mapped IPv6 address needs up to INET6_ADDRSTRLEN (46).
46;
@@ -155,12 +154,12 @@ struct IPAddress {
if (this->is_ip4()) {
struct in_addr addr4{};
addr4.s_addr = ip_addr_.s6_addr32[3];
if (inet_ntop(AF_INET, &addr4, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr)
if (net_addr_ntop(AF_INET, &addr4, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr)
buf[0] = '\0';
return buf;
}
#endif /* CONFIG_NET_IPV4 */
if (inet_ntop(AF_INET6, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr)
if (net_addr_ntop(AF_INET6, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE) == nullptr)
buf[0] = '\0';
return buf;
}
+1 -1
View File
@@ -49,7 +49,7 @@ bool decode_mifare_classic_tlv(const std::span<const uint8_t> data, uint32_t &me
}
uint8_t idx = static_cast<uint8_t>(i);
if (idx + 4 <= data.size() && data[idx + 1] == 0xFF) {
message_length = ((0xFF & data[idx + 2]) << 8) | (0xFF & data[idx + 3]);
message_length = encode_uint16(data[idx + 2], data[idx + 3]);
message_start_index = idx + MIFARE_CLASSIC_LONG_TLV_SIZE;
} else if (idx + 2 <= data.size()) {
message_length = data[idx + 1];
+2 -2
View File
@@ -44,10 +44,10 @@ i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pr
}
// extract top 6 bits of first byte and all bits of second byte for pressure
raw_pressure = ((response[0] & 0x3F) << 8) | response[1];
raw_pressure = encode_uint16(response[0] & 0x3F, response[1]);
// extract all bytes of 3rd byte and top 3 bits of fourth byte for temperature
raw_temperature = (response[2] << 3) | ((response[3] & 0xE0) >> 5);
raw_temperature = encode_uint16(response[2], response[3]) >> 5;
return i2c::ERROR_OK;
}
+151 -8
View File
@@ -78,6 +78,10 @@ from .const import (
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_NRF,
CONF_TRANSPORT,
CONF_UDP,
FLASH_SECTOR_SIZE,
SMP_UDP_PORT,
)
from .framework import (
check_and_install,
@@ -756,6 +760,15 @@ def _enter_open_dfu(host: str) -> str:
)
def has_network_ota(config: ConfigType) -> bool:
"""True when zephyr_mcumgr OTA listens on UDP."""
return any(
ota.get(CONF_PLATFORM) == "zephyr_mcumgr"
and ota.get(CONF_TRANSPORT, {}).get(CONF_UDP)
for ota in config.get(CONF_OTA, [])
)
def upload_program(config: ConfigType, args, host: str) -> bool:
from esphome.__main__ import check_permissions
from esphome.upload_targets import PortType, get_port_type
@@ -855,32 +868,91 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
raise EsphomeError(f"Upload failed with result: {result}")
return True # Handled: PYOCD upload
file = getattr(args, "file", None)
if mcumgr_device:
# MCUboot over USB CDC: the serial port speaks the MCUmgr SMP protocol
asyncio.run(_smpmgr_upload(mcumgr_device))
asyncio.run(_smpmgr_upload(mcumgr_device, file))
return True # Handled: mcumgr serial upload
# Deferred imports: bleak and friends are heavy, only load for BLE paths
from .ble_logger import is_ble_address
if host == "BLE" or is_ble_address(host):
asyncio.run(_upload_over_ble(config, host))
asyncio.run(_upload_over_ble(config, host, file))
return True # Handled: BLE OTA upload
if get_port_type(host) == PortType.NETWORK and has_network_ota(config):
ipv4 = config.get(network.DOMAIN, {}).get(network.CONF_ENABLE_IPV4, False)
addresses = _resolve_smp_udp_hosts(host, ipv4)
asyncio.run(_upload_over_udp(host, addresses, file))
return True # Handled: mcumgr UDP upload
return False # Not handled: let caller try default upload methods
async def _smpmgr_upload(device: str) -> None:
"""Upload the MCUboot application image over the MCUmgr SMP protocol."""
async def _smpmgr_upload(
device: str, file: str | None = None, udp: bool = False
) -> None:
"""Upload the MCUboot application image (or --file) over the MCUmgr SMP protocol."""
from .ota import smpmgr_upload
firmware = Path(
CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin")
file or CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin")
).resolve()
await smpmgr_upload(device, firmware)
await smpmgr_upload(device, firmware, udp)
async def _upload_over_ble(config: ConfigType, host: str) -> None:
def _resolve_smp_udp_hosts(host: str, ipv4: bool) -> list[str]:
"""Every address for the host through the address cache, in resolve_ip_address order.
The device listens on IPv6, and on IPv4 only with network enable_ipv4.
"""
import socket
from esphome.helpers import resolve_ip_address
# The CLI hands over only the first address it expanded from the cache
cache = CORE.address_cache
if cache and CORE.address and host in (cache.get_addresses(CORE.address) or ()):
host = CORE.address
resolved = resolve_ip_address(host, SMP_UDP_PORT, cache)
if not resolved:
raise EsphomeError(f"No addresses to connect to for {host}")
if not ipv4:
resolved = [addr for addr in resolved if addr[0] == socket.AF_INET6]
if not resolved:
raise EsphomeError(
f"{host} has no IPv6 address; the device listens for OTA on IPv6 "
"unless 'network: enable_ipv4: true' is set"
)
addresses: list[str] = []
for family, _, _, _, sockaddr in resolved:
address = sockaddr[0]
if family == socket.AF_INET6 and sockaddr[3]:
address = f"{address}%{sockaddr[3]}"
if address not in addresses:
addresses.append(address)
return addresses
async def _upload_over_udp(host: str, addresses: list[str], file: str | None) -> None:
"""Try each address until one answers; a failure after that is final."""
from .ota_udp import SMPUnreachableError
for index, address in enumerate(addresses, 1):
_LOGGER.info("Uploading to %s at %s", host, address)
try:
await _smpmgr_upload(address, file, udp=True)
return
except SMPUnreachableError as err:
if index == len(addresses):
raise
_LOGGER.warning("%s, trying the next address", err)
async def _upload_over_ble(
config: ConfigType, host: str, file: str | None = None
) -> None:
"""Upload over the OTA protocol the firmware was built with."""
from .ota import smpmgr_scan
@@ -890,7 +962,7 @@ async def _upload_over_ble(config: ConfigType, host: str) -> None:
ota.get(CONF_PLATFORM) == "adafruit_ble" for ota in config.get(CONF_OTA, [])
):
address = await smpmgr_scan(CORE.name) if host == "BLE" else host
await _smpmgr_upload(address)
await _smpmgr_upload(address, file)
return
from esphome.components.adafruit_ble import dfu
@@ -1125,6 +1197,74 @@ def _copy_if_exists(src: Path, dst: Path) -> None:
shutil.copy2(src, dst)
# nRF52 flash write size and MCUboot's trailer layout (bootutil_misc.c)
FLASH_WRITE_SIZE = 4
MCUBOOT_SWAP_MOVE_STATES = 3
MCUBOOT_TRAILER_INFO_SIZE = 4 * 8 + 16 # swap info fields and magic
def _mcuboot_swap_limit(partitions: dict, mcuboot_config: str) -> int | None:
"""The largest image MCUboot's swap using move can swap in, or None for other modes.
Mirrors app_max_size() in swap_move.c: the primary slot less its trailer sectors
and the sector the move needs, and no more than the secondary slot.
"""
if "CONFIG_BOOT_SWAP_USING_MOVE=y" not in mcuboot_config.splitlines():
return None
match = re.search(
r"^CONFIG_BOOT_MAX_IMG_SECTORS=(\d+)$", mcuboot_config, re.MULTILINE
)
if match is None:
_LOGGER.warning(
"Skipping the MCUboot slot size check: BOOT_MAX_IMG_SECTORS is not set"
)
return None
trailer = (
int(match.group(1)) * MCUBOOT_SWAP_MOVE_STATES * FLASH_WRITE_SIZE
+ MCUBOOT_TRAILER_INFO_SIZE
)
trailer_sectors = -(-trailer // FLASH_SECTOR_SIZE)
primary_sectors = partitions["mcuboot_primary"]["size"] // FLASH_SECTOR_SIZE
primary = (primary_sectors - trailer_sectors - 1) * FLASH_SECTOR_SIZE
return min(primary, partitions["mcuboot_secondary"]["size"])
def _check_mcuboot_swap_limit(build_dir: Path, image: Path) -> None:
"""Fail a build whose image MCUboot would refuse to swap in.
The linker only checks the app partition. MCUboot then boots the old image and
leaves the new one pending, which blocks every later OTA update.
"""
partitions_file = build_dir / "partitions.yml"
mcuboot_config = build_dir / "mcuboot" / "zephyr" / ".config"
if not image.is_file():
# A stale app_update.bin from an earlier build could otherwise go out unnoticed
_LOGGER.warning("Skipping the MCUboot slot size check: %s is missing", image)
return
if not (partitions_file.is_file() and mcuboot_config.is_file()):
_LOGGER.warning(
"Skipping the MCUboot slot size check: %s or %s is missing",
partitions_file,
mcuboot_config,
)
return
import yaml
limit = _mcuboot_swap_limit(
yaml.safe_load(partitions_file.read_text()), mcuboot_config.read_text()
)
if limit is None:
return
size = image.stat().st_size
if size > limit:
raise EsphomeError(
f"The application image is {size} bytes, but MCUboot can only swap in "
f"{limit} bytes on this board. Remove at least {size - limit} bytes, for "
"example by leaving out components or lowering the log level."
)
_LOGGER.info("Application image: %d of %d bytes MCUboot can swap in", size, limit)
def _west_build_command(
python_executable: Path, board: str, build_dir: Path, source_dir: Path
) -> list[str]:
@@ -1230,6 +1370,9 @@ def run_compile(args, config: ConfigType) -> bool:
west_out = zephyr_dir
else:
west_out = zephyr_dir / "zephyr"
if zephyr_data().get(KEY_SYSBUILD):
# Before the copies, so a failed check leaves no oversized app_update.bin
_check_mcuboot_swap_limit(build_dir, west_out / "zephyr.signed.bin")
_copy_if_exists(west_out / "zephyr.uf2", zephyr_dir / "zephyr.uf2")
_copy_if_exists(west_out / "zephyr.signed.bin", zephyr_dir / "app_update.bin")
_copy_if_exists(west_out / "zephyr.hex", zephyr_dir / "zephyr.hex")
+14
View File
@@ -18,3 +18,17 @@ AIN_TO_GPIO = {
"AIN6": 30,
"AIN7": 31,
}
FLASH_SECTOR_SIZE = 0x1000 # nRF52 flash page
# zephyr_mcumgr OTA keys, shared with the upload path
CONF_TRANSPORT = "transport"
CONF_UDP = "udp"
# SMP over UDP fits each frame in Thread's IPv6 minimum link MTU. The host passes it to
# smpclient, which subtracts the IP and UDP headers; the device sizes its receive buffer
# for the largest payload that leaves. The port and header sizes match smpclient's.
SMP_UDP_LINK_MTU = 1280
SMP_UDP_PORT = 1337 # smpclient and MCUMGR_TRANSPORT_UDP_PORT default
IPV4_UDP_HEADERS = 28
IPV6_UDP_HEADERS = 48
+132 -25
View File
@@ -1,5 +1,7 @@
import asyncio
from collections.abc import Callable
from dataclasses import asdict
from functools import partial
import json
import logging
from pathlib import Path
@@ -7,7 +9,7 @@ from pathlib import Path
from bleak.exc import BleakDBusError, BleakDeviceNotFoundError
from smp.exceptions import SMPBadStartDelimiter
from smpclient import SMPClient
from smpclient.generics import error, success
from smpclient.generics import SMPRequest, error, success
from smpclient.mcuboot import IMAGE_TLV, ImageInfo, MCUBootImageError, TLVNotFound
from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite
from smpclient.requests.os_management import ResetWrite
@@ -23,6 +25,7 @@ from esphome.core import EsphomeError
from esphome.espota2 import ProgressBar
from .ble_logger import find_device_by_name, is_ble_address
from .const import SMP_UDP_LINK_MTU, SMP_UDP_PORT
BLE_SCAN_TIMEOUT = 10.0 # seconds
RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute
@@ -51,9 +54,12 @@ async def smpmgr_scan(name: str) -> str:
return device.address
async def smpmgr_upload(device: str, firmware: Path) -> None:
async def smpmgr_upload(device: str, firmware: Path, udp: bool = False) -> None:
from .ota_udp import _quiet_smpclient_timeouts
try:
await _smpmgr_upload(device, firmware)
with _quiet_smpclient_timeouts(udp):
await _smpmgr_upload(device, firmware, udp)
except SMPTransportDisconnected as exc:
raise EsphomeError(f"{device} was disconnected.") from exc
except SMPBLETransportDeviceNotFound as exc:
@@ -83,10 +89,23 @@ def _get_image_tlv_sha256(file: Path) -> bytes:
return image_tlv_sha256.value
async def _smpmgr_upload(device: str, firmware: Path) -> None:
async def _smpmgr_upload(device: str, firmware: Path, udp: bool) -> None:
from .ota_udp import (
SMP_UDP_PROBE_TIMEOUT,
SMPUnreachableError,
_SequenceMatchingUDPTransport,
)
image_tlv_sha256 = _get_image_tlv_sha256(firmware)
if is_ble_address(device):
if udp:
smp_client = SMPClient(
_SequenceMatchingUDPTransport(mtu=SMP_UDP_LINK_MTU),
device,
# Short, so a dead address fails fast; uploads pass their own timeout
timeout_s=SMP_UDP_PROBE_TIMEOUT,
)
elif is_ble_address(device):
smp_client = SMPClient(SMPBLETransport(), device)
else:
smp_client = SMPClient(SMPSerialTransport(), device)
@@ -94,6 +113,10 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None:
_LOGGER.info("Connecting %s...", device)
try:
await smp_client.connect()
except OSError as exc:
if not udp:
raise
raise SMPUnreachableError(f"Cannot reach {device}: {exc}") from exc
except BleakDeviceNotFoundError as exc:
raise EsphomeError(f"Device {device} not found") from exc
except BleakDBusError as exc:
@@ -108,17 +131,78 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None:
_LOGGER.info("Connected %s...", device)
try:
await _smpmgr_upload_connected(smp_client, device, firmware, image_tlv_sha256)
await _smpmgr_upload_connected(
smp_client, device, firmware, image_tlv_sha256, udp
)
finally:
await smp_client.disconnect()
async def _smpmgr_upload_connected(
smp_client: SMPClient, device: str, firmware: Path, image_tlv_sha256: bytes
) -> None:
def _find_image(state: object, image_hash: bytes) -> object | None:
"""Our image in an image state reply, or None."""
return next((image for image in state.images if image.hash == image_hash), None)
async def _try_request(
smp_client: SMPClient, request: SMPRequest, timeout_s: float | None
) -> object | None:
"""The reply, or None when none arrives in time."""
try:
image_state = await smp_client.request(ImageStatesRead(), 2.5)
except (SMPBadStartDelimiter, TimeoutError) as exc:
return await smp_client.request(request, timeout_s)
except TimeoutError:
return None
async def _request_with_retry(
smp_client: SMPClient,
make_request: Callable[[], SMPRequest],
timeout_s: float | None,
retries: int,
target: str,
):
"""Send a fresh request (new sequence number) each attempt until one is answered."""
for attempt in range(retries + 1):
if (
reply := await _try_request(smp_client, make_request(), timeout_s)
) is not None:
return reply
if attempt < retries:
_LOGGER.debug("No reply, resending (%d/%d)", attempt + 1, retries)
name = getattr(make_request, "func", make_request).__name__
raise EsphomeError(
f"{target} could not be reached: no reply to {name} after {retries + 1} attempts"
)
async def _smpmgr_upload_connected(
smp_client: SMPClient,
device: str,
firmware: Path,
image_tlv_sha256: bytes,
udp: bool,
) -> None:
from .ota_udp import (
SMP_UDP_REQUEST_TIMEOUT,
SMP_UDP_STATE_RETRIES,
SMPUnreachableError,
_udp_reset,
_udp_upload,
)
# UDP state reads use the client's short timeout; BLE and serial keep their tuned ones
read_timeout, write_timeout, retries = (
(None, SMP_UDP_REQUEST_TIMEOUT, SMP_UDP_STATE_RETRIES) if udp else (2.5, 1.0, 0)
)
target = f"{device} on UDP port {SMP_UDP_PORT}" if udp else device
try:
image_state = await _request_with_retry(
smp_client, ImageStatesRead, read_timeout, retries, target
)
except SMPBadStartDelimiter as exc:
raise EsphomeError(f"mcumgr is not supported by device ({device})") from exc
except EsphomeError as exc:
if udp:
raise SMPUnreachableError(str(exc)) from exc
raise EsphomeError(f"mcumgr is not supported by device ({device})") from exc
already_uploaded = False
@@ -126,22 +210,32 @@ async def _smpmgr_upload_connected(
if error(image_state):
raise EsphomeError(f"Failed to read image state from {device}: {image_state}")
if success(image_state):
if len(image_state.images) == 0:
images = image_state.images
if len(images) == 0:
_LOGGER.warning("No images on device!")
for image in image_state.images:
for image in images:
_LOGGER.info(
"Image state:\n%s",
json.dumps(image, indent=2, default=_json_state),
)
if image.active and not image.confirmed:
raise EsphomeError("No free slot. Testing mode but not confirmed yet.")
if image.hash == image_tlv_sha256:
if already_uploaded:
raise EsphomeError("Both slots have the same image already")
if image.confirmed:
raise EsphomeError("The same image already confirmed")
_LOGGER.warning("The same image already uploaded")
already_uploaded = True
if sum(image.hash == image_tlv_sha256 for image in images) > 1:
raise EsphomeError("Both slots have the same image already")
if (ours := _find_image(image_state, image_tlv_sha256)) is not None:
if ours.confirmed:
raise EsphomeError("The same image already confirmed")
_LOGGER.warning("The same image already uploaded")
already_uploaded = True
elif any(image.pending and not image.active for image in images):
# The device refuses to erase or overwrite a pending slot (NO_FREE_SLOT).
# The image states carry no size, so the cause cannot be told from here.
raise EsphomeError(
f"{device} still has an earlier update pending. Restart the device to "
"apply it; if it then still runs the old firmware, the image was probably "
"too large for the MCUboot slot and the device must be flashed once over "
"USB to clear it."
)
if not already_uploaded:
with firmware.open("rb") as file:
@@ -150,20 +244,33 @@ async def _smpmgr_upload_connected(
progress = ProgressBar("Uploading")
progress.update(0)
try:
async for offset in smp_client.upload(image):
progress.update(offset / upload_size)
if udp:
await _udp_upload(
smp_client, image, progress, target, image_tlv_sha256
)
else:
async for offset in smp_client.upload(image):
progress.update(offset / upload_size)
finally:
progress.done()
_LOGGER.info("Mark image for testing")
r = await smp_client.request(ImageStatesWrite(hash=image_tlv_sha256), 1.0)
r = await _request_with_retry(
smp_client,
partial(ImageStatesWrite, hash=image_tlv_sha256),
write_timeout,
retries,
target,
)
if error(r):
raise EsphomeError(f"Failed to mark image for testing on {device}: {r}")
await asyncio.sleep(RESET_DELAY)
_LOGGER.info("Reset")
r = await smp_client.request(ResetWrite(), 1.0)
if udp:
await _udp_reset(smp_client, device, image_tlv_sha256)
return
r = await smp_client.request(ResetWrite(), write_timeout)
if error(r):
raise EsphomeError(f"Failed to reset {device}: {r}")
+250
View File
@@ -0,0 +1,250 @@
"""SMP over UDP for nRF52 OTA: the lossy-link pieces of the MCUmgr upload."""
import asyncio
from collections.abc import Iterator
from contextlib import contextmanager
from functools import partial
from hashlib import sha256
import logging
from smp import header as smpheader
from smpclient import SMPClient
from smpclient.generics import error, success
from smpclient.requests.image_management import ImageStatesRead, ImageUploadWrite
from smpclient.requests.os_management import ResetWrite
from smpclient.transport.udp import SMPUDPTransport
from esphome.core import EsphomeError
from esphome.espota2 import ProgressBar
from .ota import _find_image, _request_with_retry, _try_request
# Thread is lossy and each chunk waits on a flash write synced with the radio
SMP_UDP_REQUEST_TIMEOUT = 8.0
SMP_UDP_UPLOAD_RETRIES = 15
SMP_UDP_STATE_RETRIES = 3
SMP_UDP_PROBE_TIMEOUT = 2.0
# MCUboot moves the whole image during the swap, then the device rejoins the network;
# 50 to 61 s on an nRF52840 over Thread, so leave room
SMP_UDP_REBOOT_WAIT = 120.0
# After a lost reset reply: two probes tell an old image still running from a reboot
SMP_UDP_LOST_RESET_WAIT = 2 * SMP_UDP_PROBE_TIMEOUT
SMP_UDP_RESET_ATTEMPTS = 2 # the reset and at most one resend
_LOGGER = logging.getLogger(__name__)
class SMPUnreachableError(EsphomeError):
"""The device did not answer over UDP before anything was sent to it."""
class _SequenceMatchingUDPTransport(SMPUDPTransport):
"""Skips late replies to earlier attempts, which stay queued after a timeout."""
async def send_and_receive(self, data: bytes) -> bytes:
await self.send(data)
sequence = smpheader.Header.loads(data[: smpheader.Header.SIZE]).sequence
while True:
frame = await self.receive()
if (
smpheader.Header.loads(frame[: smpheader.Header.SIZE]).sequence
== sequence
):
return frame
_LOGGER.debug("Skipping a late reply to an earlier request")
def _drop_timeout_errors(record: logging.LogRecord) -> bool:
"""Drop smpclient's ERROR record, payload included, for each timed-out request."""
return not (
record.levelno == logging.ERROR and record.getMessage().startswith("Timeout (")
)
@contextmanager
def _quiet_smpclient_timeouts(enabled: bool) -> Iterator[None]:
if not enabled:
yield
return
smpclient_logger = logging.getLogger("smpclient")
smpclient_logger.addFilter(_drop_timeout_errors)
try:
yield
finally:
smpclient_logger.removeFilter(_drop_timeout_errors)
async def _image_in_slot(smp_client: SMPClient, image_hash: bytes, target: str) -> bool:
state = await _request_with_retry(
smp_client, ImageStatesRead, None, SMP_UDP_STATE_RETRIES, target
)
if not success(state):
_LOGGER.warning(
"Could not read the image states, sending the image again: %s", state
)
return False
if _find_image(state, image_hash) is not None:
return True
_LOGGER.warning("The device does not list the image, sending it again")
return False
async def _udp_upload(
smp_client: SMPClient,
image: bytes,
progress: ProgressBar,
target: str,
image_hash: bytes,
) -> None:
"""Upload an image, resending each chunk until the device answers.
smpclient's upload() gives up on the first lost reply. mcumgr ignores a write at
an unexpected offset and replies with the offset it wants, so resending is safe.
"""
total = len(image)
first = {"len": total, "image": 0, "sha": sha256(image).digest()}
off = 0
highest = 0
stalls = 0
while off < total:
fields = first if off == 0 else {}
_, size = smp_client.get_max_cbor_and_data_size(
ImageUploadWrite(off=off, data=b"", **fields)
)
make_request = partial(
ImageUploadWrite, off=off, data=image[off : off + size], **fields
)
try:
response = await _request_with_retry(
smp_client,
make_request,
SMP_UDP_REQUEST_TIMEOUT,
SMP_UDP_UPLOAD_RETRIES,
target,
)
except EsphomeError as exc:
raise EsphomeError(f"Upload stalled at offset {off}: {exc}") from exc
if error(response):
raise EsphomeError(f"Upload failed at offset {off}: {response}")
if response.off is None:
raise EsphomeError(f"No offset received at {off}: {response}")
if response.off > total:
raise EsphomeError(f"Device reported offset {response.off} past {total}")
# mcumgr clears its upload state after the last write, so a resent final chunk
# whose first reply was lost comes back with offset 0
if (
response.off == 0
and off + size >= total
and await _image_in_slot(smp_client, image_hash, target)
):
_LOGGER.info(
"The reply to the last chunk was lost; the device lists the image"
)
progress.update(1)
return
# Follow the device's offset, so an upload it restarted is sent again. Only a
# new high resets the count, so stalls and repeated restarts both give up.
if response.off > highest:
highest = response.off
stalls = 0
elif response.off <= off:
stalls += 1
if stalls > SMP_UDP_UPLOAD_RETRIES:
raise EsphomeError(f"Upload not advancing past offset {highest}")
off = response.off
progress.update(off / total)
# match needs CONFIG_IMG_ENABLE_IMAGE_CHECK, which NCS cannot build with its PSA crypto.
# Marking the image only finds the slot by its header hash; MCUboot checks the whole
# image before it boots it.
if response.match is False:
raise EsphomeError("Upload failed: device reported mismatched SHA256")
if response.match is None:
_LOGGER.info(
"The device did not verify the image hash; MCUboot checks it at boot"
)
async def _poll_for_new_image(
smp_client: SMPClient, image_hash: bytes, wait_s: float
) -> tuple[object | None, bool]:
"""Poll the image states after a reset.
Returns the last reply (None if there was none) and whether the device went
silent after it. Stops at the new image, at the first good reply after a
silence (the device rebooted), or after wait_s.
"""
# Also skips a reply the old image sends before it reboots
await asyncio.sleep(SMP_UDP_PROBE_TIMEOUT)
loop = asyncio.get_running_loop()
deadline = loop.time() + wait_s
state = None
rebooted = silent = False
while loop.time() < deadline:
reply = await _try_request(smp_client, ImageStatesRead(), SMP_UDP_PROBE_TIMEOUT)
if reply is None:
rebooted = silent = True
continue
state = reply
silent = False
if success(reply) and (
rebooted or getattr(_find_image(reply, image_hash), "active", False)
):
break
return state, silent
async def _udp_reset(smp_client: SMPClient, device: str, image_hash: bytes) -> None:
"""Reset the device and check that it comes back running the new image.
Resends only when the old image still runs: a blind resend could reboot the new
image before it confirms itself, and MCUboot would roll the update back. MCUboot
boots the old image again when it cannot swap the new one in, and the new image
then stays pending, so every later upload fails until the slot is cleared.
"""
for attempt in range(SMP_UDP_RESET_ATTEMPTS):
reply = await _try_request(smp_client, ResetWrite(), SMP_UDP_REQUEST_TIMEOUT)
if reply is not None and error(reply):
raise EsphomeError(f"Failed to reset {device}: {reply}")
rebooting = reply is not None
state, silent = await _poll_for_new_image(
smp_client,
image_hash,
SMP_UDP_REBOOT_WAIT if rebooting else SMP_UDP_LOST_RESET_WAIT,
)
if silent and not rebooting:
# Quiet after a reset with no reply, even after an earlier answer from the
# old image: it took effect, so wait for the reboot instead of resending
rebooting = True
state, silent = await _poll_for_new_image(
smp_client, image_hash, SMP_UDP_REBOOT_WAIT
)
if state is None or silent:
# Silence after a reset most likely means it is still swapping or rejoining
_LOGGER.warning(
"%s did not answer after the reset; check that it runs the new image",
device,
)
return
if not success(state):
raise EsphomeError(
f"Could not read the image states of {device} after the reset, it "
f"answered with an error: {state}"
)
ours = _find_image(state, image_hash)
if ours is not None and ours.active:
_LOGGER.info("%s runs the new image", device)
return
if rebooting:
raise EsphomeError(
f"{device} rebooted but did not swap in the new image; it may be too "
f"large for the MCUboot slot: {state}"
)
if ours is None or not ours.pending:
raise EsphomeError(f"{device} no longer has the new image pending: {state}")
if attempt == SMP_UDP_RESET_ATTEMPTS - 1:
raise EsphomeError(
f"{device} still runs the old image with the new one pending; it did not "
"answer the reset. Restart the device to apply the update."
)
# Resetting the old image is harmless, even if it rebooted and could not swap
_LOGGER.info("%s still runs the old image, resending the reset", device)
+16 -22
View File
@@ -1,29 +1,23 @@
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import button, output
import esphome.config_validation as cv
from esphome.const import CONF_DURATION, CONF_ID, CONF_OUTPUT
from esphome.const import CONF_DURATION, CONF_OUTPUT
from esphome.types import ConfigType
from .. import output_ns
OutputButton = output_ns.class_("OutputButton", button.Button, cg.Component)
CONFIG_SCHEMA = (
button.button_schema(OutputButton)
.extend(
{
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Required(CONF_DURATION): cv.positive_time_period_milliseconds,
}
)
.extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = button.button_schema(button.ApplyButton).extend(
{
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Required(CONF_DURATION): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_duration(config[CONF_DURATION]))
await cg.add_variable(var.set_output, config[CONF_OUTPUT])
await cg.register_component(var, config)
await button.register_button(var, config)
async def to_code(config: ConfigType) -> None:
out = parent_ref(await cg.get_variable(config[CONF_OUTPUT]))
duration = config[CONF_DURATION].total_milliseconds
await button.sub_buttons(config).create(
config,
None,
apply=f"output::press_output_button(entity, {out}, {duration})",
)
@@ -1,21 +1,12 @@
#include "output_button.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::output {
ESPHOME_LOG_TAG(TAG, "output.button");
static constexpr uint32_t RESET_TIMEOUT_ID = 0;
void OutputButton::dump_config() {
LOG_BUTTON("", "Output Button", this);
ESP_LOGCONFIG(TAG, " Duration: %.1fs", this->duration_ / 1e3f);
}
void OutputButton::press_action() {
this->output_->turn_on();
// One timeout id, so a second press restarts the reset instead of stacking another
this->set_timeout(RESET_TIMEOUT_ID, this->duration_, [this]() { this->output_->turn_off(); });
void press_output_button(button::Button *button, BinaryOutput *output, uint32_t duration) {
output->turn_on();
// Keyed on the button, so a second press replaces the pending reset instead of stacking another
App.scheduler.set_timeout(button, duration, [output]() { output->turn_off(); });
}
} // namespace esphome::output
@@ -1,23 +1,13 @@
#pragma once
#include "esphome/core/component.h"
#include <cstdint>
#include "esphome/components/button/button.h"
#include "esphome/components/output/binary_output.h"
namespace esphome::output {
class OutputButton final : public button::Button, public Component {
public:
void dump_config() override;
void set_output(BinaryOutput *output) { output_ = output; }
void set_duration(uint32_t duration) { duration_ = duration; }
protected:
void press_action() override;
output::BinaryOutput *output_;
uint32_t duration_;
};
/// Turn the output on and off again after duration ms; a second press restarts the wait.
void press_output_button(button::Button *button, BinaryOutput *output, uint32_t duration);
} // namespace esphome::output
+1 -1
View File
@@ -1071,7 +1071,7 @@ void PN71xx::card_emu_t4t_get_response_(const std::span<const uint8_t> response,
ok = true;
} else if (apdu_starts_with(CARD_EMU_T4T_READ, sizeof(CARD_EMU_T4T_READ)) && apdu_size == 5) {
// READ BINARY: CLA INS P1 P2 Le, where P1-P2 is the offset
const uint16_t offset = (apdu_begin[2] << 8) | apdu_begin[3];
const uint16_t offset = encode_uint16(apdu_begin[2], apdu_begin[3]);
const uint8_t length = apdu_begin[4];
if (this->ce_state_ == CardEmulationState::CARD_EMU_CC_SELECTED) {
ESP_LOGVV(TAG, "CARD_EMU_T4T_READ with CARD_EMU_CC_SELECTED");
+4 -6
View File
@@ -45,7 +45,7 @@ void PZEM004T::loop() {
break;
}
case 0xA0: { // Voltage Response
uint16_t int_voltage = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0);
uint16_t int_voltage = encode_uint16(resp[1], resp[2]);
float voltage = int_voltage + (resp[3] / 10.0f);
PUBLISH_STATE(this->voltage_sensor_, voltage);
ESP_LOGD(TAG, "Got Voltage %.1f V", voltage);
@@ -53,7 +53,7 @@ void PZEM004T::loop() {
break;
}
case 0xA1: { // Current Response
uint16_t int_current = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0);
uint16_t int_current = encode_uint16(resp[1], resp[2]);
float current = int_current + (resp[3] / 100.0f);
PUBLISH_STATE(this->current_sensor_, current);
ESP_LOGD(TAG, "Got Current %.2f A", current);
@@ -61,7 +61,7 @@ void PZEM004T::loop() {
break;
}
case 0xA2: { // Active Power Response
uint16_t power = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0);
uint16_t power = encode_uint16(resp[1], resp[2]);
PUBLISH_STATE(this->power_sensor_, power);
ESP_LOGD(TAG, "Got Power %u W", power);
this->write_state_(READ_ENERGY);
@@ -103,9 +103,7 @@ void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) {
data[3] = 1;
data[4] = 1;
data[5] = 0;
data[6] = 0;
for (int i = 0; i < 6; i++)
data[6] += data[i];
data[6] = sum_bytes(data.data(), 6);
this->write_array(data);
this->read_state_ = state;
+2 -2
View File
@@ -33,7 +33,7 @@ static constexpr int16_t SPEED_SENTINEL_256 = 256;
// Decode coordinate/speed value from RD-03D format
// Per datasheet: MSB=1 means positive, MSB=0 means negative
static constexpr int16_t decode_value(uint8_t low_byte, uint8_t high_byte) {
int16_t value = ((high_byte & 0x7F) << 8) | low_byte;
int16_t value = encode_uint16(high_byte & 0x7F, low_byte);
if ((high_byte & 0x80) == 0) {
value = -value;
}
@@ -150,7 +150,7 @@ void RD03DComponent::process_frame_() {
int16_t x = decode_value(x_low, x_high);
int16_t y = decode_value(y_low, y_high);
int16_t speed = decode_value(speed_low, speed_high);
uint16_t resolution = (res_high << 8) | res_low;
uint16_t resolution = encode_uint16(res_high, res_low);
// Check if target is present
// Requires non-zero coordinates AND valid speed (not a sentinel value)
@@ -50,12 +50,7 @@ class PDPioneerData {
const uint8_t &operator[](size_t idx) const { return this->data_[idx]; }
protected:
uint8_t checksum_() const {
uint8_t cs = this->is_odd_burst() ? 15 : 0;
for (uint8_t idx = 0; idx < OFFSET_CS; idx++)
cs += this->data_[idx];
return cs;
}
uint8_t checksum_() const { return sum_bytes(this->data_.data(), OFFSET_CS, this->is_odd_burst() ? 15 : 0); }
std::array<uint8_t, FRAME_SIZE> data_{};
};
+2 -9
View File
@@ -31,14 +31,7 @@ void SenseAirComponent::update() {
response[8], response[9], response[10], response[11], response[12]);
this->status_set_warning();
while (this->available()) {
uint8_t b;
if (this->read_byte(&b)) {
ESP_LOGV(TAG, " ... %02x", b);
} else {
ESP_LOGV(TAG, " ... nothing read");
}
}
this->discard_available();
return;
}
@@ -121,7 +114,7 @@ void SenseAirComponent::abc_get_period() {
return;
}
const uint16_t hours = (uint16_t(response[3]) << 8) | response[4];
const uint16_t hours = encode_uint16(response[3], response[4]);
ESP_LOGD(TAG, "SenseAir Read ABC Period: %u hours", hours);
}
+17
View File
@@ -8,6 +8,7 @@
#include "esphome/components/sensor/filter.h"
#endif
#include <cmath>
#include <initializer_list>
#include <memory>
@@ -165,4 +166,20 @@ class Sensor : public EntityBase {
} sensor_flags_{};
};
/** Read another sensor as an input, such as the temperature a gas sensor compensates for.
*
* @return The state of `sensor` when it lies in [min, max] (both included); NAN when `sensor` is null, its
* state is NAN (as it is before the first publish) or its state is out of range.
* The caller decides whether to skip or fall back, and what to log.
*/
inline float source_value(const Sensor *sensor, float min, float max) {
if (sensor == nullptr)
return NAN;
const float value = sensor->state;
// isnan first: callers test the result with isnan, and the compiler can reuse this check
if (std::isnan(value) || value < min || value > max)
return NAN;
return value;
}
} // namespace esphome::sensor
@@ -339,7 +339,7 @@ int ShellyDimmer::handle_byte_(uint8_t c) {
if (pos == 4 + payload_len + 1) {
// Verify checksum.
const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
const uint16_t csum = encode_uint16(this->buffer_[pos - 1], c);
const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
if (csum != csum_verify) {
return -1;
+9 -3
View File
@@ -58,7 +58,8 @@ LAT_LON_REGEX = re.compile(
r"(?:([0-9]+)\s*°)?\s*"
r"(?:([0-9]+)\s*[′\'])?\s*"
r'(?:([0-9]+)\s*[″"])?\s*'
r"([NESW])?"
r"([NESW])?",
re.IGNORECASE,
)
@@ -72,7 +73,7 @@ def parse_latlon(value: Any) -> float:
pass
value = cv.string_strict(value)
m = LAT_LON_REGEX.match(value)
m = LAT_LON_REGEX.fullmatch(value.strip())
if m is None:
raise cv.Invalid("Invalid format for latitude/longitude")
@@ -82,10 +83,15 @@ def parse_latlon(value: Any) -> float:
second = m.group(4)
d = m.group(5)
if deg is None and minute is None and second is None:
raise cv.Invalid("Invalid format for latitude/longitude")
if sign == "-" and d:
raise cv.Invalid("Use either a minus sign or a hemisphere letter, not both")
val = float(deg or 0) + float(minute or 0) / 60 + float(second or 0) / 3600
if sign == "-":
val *= -1
if d and d in "SW":
if d and d.upper() in "SW":
val *= -1
return val
+2 -2
View File
@@ -76,12 +76,12 @@ void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t
this->log_(level, tag, message, message_len);
}
void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const {
void Syslog::log_(const uint8_t level, const char *tag, const char *message, size_t message_len) const {
if (level > this->log_level_)
return;
// Syslog PRI calculation: facility * 8 + severity
int severity = 7;
if ((unsigned) level <= 7) {
if (level <= 7) {
severity = progmem_read_byte(&LOG_LEVEL_TO_SYSLOG_SEVERITY[level]);
}
int pri = this->facility_ * 8 + severity;
+6 -5
View File
@@ -18,19 +18,20 @@ size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *m
class Syslog final : public Component, public Parented<udp::UDPComponent> {
public:
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
Syslog(uint8_t level, time::RealTimeClock *time) : time_(time), log_level_(level) {}
void setup() override;
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len);
void set_strip(bool strip) { this->strip_ = strip; }
void set_facility(int facility) { this->facility_ = facility; }
void set_facility(uint8_t facility) { this->facility_ = facility; }
void set_format(SyslogFormat format) { this->format_ = format; }
protected:
int log_level_;
void log_(int level, const char *tag, const char *message, size_t message_len) const;
void log_(uint8_t level, const char *tag, const char *message, size_t message_len) const;
time::RealTimeClock *time_;
uint8_t log_level_;
uint8_t facility_{16};
bool strip_{true};
int facility_{16};
SyslogFormat format_{SYSLOG_FORMAT_RFC3164};
};
} // namespace esphome::syslog
+2 -1
View File
@@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/helpers.h"
namespace esphome::tcs34725 {
@@ -57,7 +58,7 @@ class TCS34725Component final : public PollingComponent, public i2c::I2CDevice {
uint8_t buffer[2];
auto retval = this->read_register(a_register, buffer, 2);
if (retval == i2c::ERROR_OK)
data = (uint16_t(buffer[1]) << 8) | (uint16_t(buffer[0]) & 0xFF);
data = encode_uint16(buffer[1], buffer[0]);
return retval;
}
i2c::ErrorCode write_config_register_(uint8_t a_register, uint8_t data) {
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff. Show more