mirror of
https://github.com/esphome/esphome.git
synced 2026-11-09 14:45:57 +00:00
Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs
# Conflicts: # tests/component_tests/output/test_output.py
This commit is contained in:
208 files changed
+8260
-3410
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,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]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,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
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_{};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in new issue
Block a user