mirror of
https://github.com/esphome/esphome.git
synced 2026-10-24 23:15:34 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b493ffc9 |
No files matched your search
@@ -195,7 +195,6 @@ esphome/components/ens160_spi/* @latonita
|
||||
esphome/components/ens210/* @itn3rd77
|
||||
esphome/components/epaper_spi/* @esphome/core
|
||||
esphome/components/es7210/* @kahrendt
|
||||
esphome/components/es7243/* @Chorty
|
||||
esphome/components/es7243e/* @kbx81
|
||||
esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
@@ -698,7 +697,6 @@ esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/uart_mux/* @kbx81
|
||||
esphome/components/uart_tcp/* @Bascht74
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/udp/packet_interface/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ufm01/* @ljungqvist
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import airthings_wave_base, sensor
|
||||
from esphome.components.const import CONF_DEVICE_TYPE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CO2,
|
||||
@@ -28,6 +27,7 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_(
|
||||
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
|
||||
)
|
||||
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType")
|
||||
DEVICE_TYPES = WaveDeviceType.options("WAVE_PLUS", "WAVE_GEN2")
|
||||
|
||||
|
||||
@@ -110,12 +110,12 @@ bool AS7341Component::read_channels(uint16_t *data) {
|
||||
this->set_smux_low_channels(true);
|
||||
this->enable_spectral_measurement(true);
|
||||
this->wait_for_data();
|
||||
bool low_success = this->read_bytes_16_le(AS7341_CH0_DATA_L, data, 6);
|
||||
bool low_success = this->read_bytes_16(AS7341_CH0_DATA_L, data, 6);
|
||||
|
||||
this->set_smux_low_channels(false);
|
||||
this->enable_spectral_measurement(true);
|
||||
this->wait_for_data();
|
||||
bool high_sucess = this->read_bytes_16_le(AS7341_CH0_DATA_L, &data[6], 6);
|
||||
bool high_sucess = this->read_bytes_16(AS7341_CH0_DATA_L, &data[6], 6);
|
||||
|
||||
return low_success && high_sucess;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ CONF_BITRATE = "bitrate"
|
||||
CONF_BLUE_CHANNEL = "blue_channel"
|
||||
CONF_BORDER_ROUTER = "border_router"
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANNEL_COLORS = "channel_colors"
|
||||
CONF_CLEAR_CHANNEL = "clear_channel"
|
||||
@@ -32,7 +31,6 @@ CONF_CONVERSION_RATE = "conversion_rate"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_DRIVE_STRENGTH = "drive_strength"
|
||||
CONF_DTR_PIN = "dtr_pin"
|
||||
@@ -55,7 +53,6 @@ CONF_LIBRETINY = "libretiny"
|
||||
CONF_LOOP = "loop"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_MAX_PACKET_SIZE = "max_packet_size"
|
||||
CONF_MODEM = "modem"
|
||||
CONF_MODULATION = "modulation"
|
||||
CONF_NOX_INDEX = "nox_index"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
|
||||
@@ -18,7 +18,6 @@ from esphome.components import (
|
||||
text_sensor,
|
||||
valve,
|
||||
)
|
||||
from esphome.components.const import CONF_BUTTONS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
@@ -168,6 +167,7 @@ VALVE_TYPES = {
|
||||
|
||||
|
||||
CONF_ALARM_CONTROL_PANELS = "alarm_control_panels"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_CLIMATES = "climates"
|
||||
CONF_COVERS = "covers"
|
||||
CONF_DATETIMES = "datetimes"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
DOMAIN = "es7243"
|
||||
@@ -1,52 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.audio_adc import AudioAdc
|
||||
from esphome.components.i2s_audio import (
|
||||
CONF_I2S_AUDIO_ID,
|
||||
CONF_I2S_MCLK_PIN,
|
||||
I2SAudioComponent,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MIC_GAIN
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@Chorty"]
|
||||
DEPENDENCIES = ["i2c", "i2s_audio"]
|
||||
|
||||
es7243_ns = cg.esphome_ns.namespace("es7243")
|
||||
ES7243 = es7243_ns.class_("ES7243", AudioAdc, cg.Component, i2c.I2CDevice)
|
||||
|
||||
# Gains the chip's register 0x08 can express, from the ESP-ADF es7243 driver
|
||||
ES7243_MIC_GAINS = [1, 3.5, 18, 20.5, 22.5, 24.5, 25, 27]
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ES7243),
|
||||
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
|
||||
cv.Optional(CONF_MIC_GAIN, default="27db"): cv.All(
|
||||
cv.decibel, cv.one_of(*ES7243_MIC_GAINS)
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x13))
|
||||
)
|
||||
|
||||
|
||||
def _validate_mclk_pin(config: ConfigType) -> None:
|
||||
"""The chip only answers on I2C after it has seen MCLK, so the bus must drive that pin."""
|
||||
i2s_id = config[CONF_I2S_AUDIO_ID]
|
||||
if CONF_I2S_MCLK_PIN not in fv.get_declaring_config(i2s_id):
|
||||
raise cv.Invalid(f"es7243 needs {CONF_I2S_MCLK_PIN} on i2s_audio {i2s_id}")
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_mclk_pin
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
i2s = await cg.get_variable(config[CONF_I2S_AUDIO_ID])
|
||||
var = await i2c.new_i2c_component(config, i2s)
|
||||
|
||||
cg.add(var.set_mic_gain(config[CONF_MIC_GAIN]))
|
||||
@@ -1,89 +0,0 @@
|
||||
#include "es7243.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::es7243 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "es7243");
|
||||
|
||||
// Register map from the ES7243 datasheet
|
||||
static constexpr uint8_t REG_MODE_CONFIG = 0x00;
|
||||
static constexpr uint8_t REG_SDP_FORMAT = 0x01;
|
||||
static constexpr uint8_t REG_MUTE_CONTROL = 0x05;
|
||||
static constexpr uint8_t REG_STATE_CONTROL = 0x06;
|
||||
static constexpr uint8_t REG_GAIN = 0x08; // ANACTRL1
|
||||
static constexpr uint8_t MCLK_PULSES = 20;
|
||||
|
||||
struct GainEntry {
|
||||
float db;
|
||||
uint8_t reg;
|
||||
};
|
||||
// Register 0x08 values for each gain, from the ESP-ADF es7243 driver
|
||||
static constexpr GainEntry GAIN_TABLE[] = {
|
||||
{1.0f, 0x11}, {3.5f, 0x13}, {18.0f, 0x21}, {20.5f, 0x23},
|
||||
{22.5f, 0x06}, {24.5f, 0x41}, {25.0f, 0x07}, {27.0f, 0x43},
|
||||
};
|
||||
static constexpr uint8_t GAIN_COUNT = sizeof(GAIN_TABLE) / sizeof(GAIN_TABLE[0]);
|
||||
|
||||
// The table entry nearest the requested gain; values outside the table snap to its ends
|
||||
static uint8_t nearest_gain_index(float gain_db) {
|
||||
uint8_t best = 0;
|
||||
for (uint8_t i = 1; i < GAIN_COUNT; i++) {
|
||||
if (std::fabs(gain_db - GAIN_TABLE[i].db) < std::fabs(gain_db - GAIN_TABLE[best].db))
|
||||
best = i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void ES7243::setup() {
|
||||
// The chip ignores I2C until it has seen MCLK, so pulse the pin before the I2S peripheral takes it over
|
||||
const gpio_num_t mclk = this->i2s_->get_pin_config().mclk;
|
||||
gpio_reset_pin(mclk);
|
||||
gpio_set_direction(mclk, GPIO_MODE_OUTPUT);
|
||||
for (uint8_t i = 0; i < MCLK_PULSES; i++) {
|
||||
gpio_set_level(mclk, 0);
|
||||
delay(1);
|
||||
gpio_set_level(mclk, 1);
|
||||
delay(1);
|
||||
}
|
||||
|
||||
// The ESP-ADF sequence: mute while the format and gain are set, then unmute
|
||||
const bool ok = this->write_byte(REG_MODE_CONFIG, 0x01) && this->write_byte(REG_STATE_CONTROL, 0x00) &&
|
||||
this->write_byte(REG_MUTE_CONTROL, 0x1B) && this->write_byte(REG_SDP_FORMAT, 0x0C) &&
|
||||
this->write_mic_gain_() && this->write_byte(REG_MUTE_CONTROL, 0x13);
|
||||
if (!ok) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->setup_complete_ = true;
|
||||
}
|
||||
|
||||
void ES7243::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ES7243 audio ADC:\n"
|
||||
" Mic gain: %.1f dB",
|
||||
this->mic_gain());
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
float ES7243::mic_gain() { return GAIN_TABLE[this->gain_index_].db; }
|
||||
|
||||
bool ES7243::set_mic_gain(float mic_gain) {
|
||||
this->gain_index_ = nearest_gain_index(mic_gain);
|
||||
if (!this->setup_complete_)
|
||||
return true;
|
||||
return this->status_comm_result(this->write_mic_gain_());
|
||||
}
|
||||
|
||||
bool ES7243::write_mic_gain_() { return this->write_byte(REG_GAIN, GAIN_TABLE[this->gain_index_].reg); }
|
||||
|
||||
} // namespace esphome::es7243
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/audio_adc/audio_adc.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/i2s_audio/i2s_audio.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::es7243 {
|
||||
|
||||
/// ES7243 microphone ADC. Not the ES7243E, which has a different register map and its own platform.
|
||||
/// Register sequence from the ESP-ADF es7243 driver.
|
||||
class ES7243 final : public audio_adc::AudioAdc, public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
explicit ES7243(const i2s_audio::I2SAudioComponent *i2s) : i2s_(i2s) {}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
// Before i2s_audio (DATA) routes the MCLK pin to the I2S peripheral, after the I2C bus
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
bool set_mic_gain(float mic_gain) override;
|
||||
float mic_gain() override;
|
||||
|
||||
protected:
|
||||
bool write_mic_gain_();
|
||||
|
||||
const i2s_audio::I2SAudioComponent *i2s_;
|
||||
uint8_t gain_index_{0}; // into GAIN_TABLE in the .cpp
|
||||
bool setup_complete_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::es7243
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -289,9 +289,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
if config[CONF_USE_PSRAM]:
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_EH_HOST_PORT_DMA_PREFER_SPIRAM", True)
|
||||
# Task stacks in PSRAM corrupt the heap on esp_hosted 3.0.10
|
||||
# (esp-hosted-mcu#254); revisit with the next esp_hosted release
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DFLT_TASK_FROM_SPIRAM", False)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DFLT_TASK_FROM_SPIRAM", True)
|
||||
|
||||
# ESP-NOW-over-hosted shim: only the radio-less ESP32-P4 host needs it (see
|
||||
# the note by _MAX_CUSTOM_MSG_HANDLERS). Enabled for every P4 host, not
|
||||
|
||||
@@ -13,7 +13,7 @@ GreeClimate = gree_ns.class_("GreeClimate", climate_ir.ClimateIR)
|
||||
|
||||
Model = gree_ns.enum("Model")
|
||||
MODELS = Model.options(
|
||||
"generic", "yan", "yaa", "yac", "yac1fb9", "yx1ff", "yag", "yaw1f", prefix="GREE_"
|
||||
"generic", "yan", "yaa", "yac", "yac1fb9", "yx1ff", "yag", prefix="GREE_"
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
|
||||
@@ -26,5 +26,3 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
if config[CONF_MODEL] == "yaw1f":
|
||||
cg.add_define("USE_GREE_YAW1F")
|
||||
@@ -2,18 +2,13 @@
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::gree {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "gree.climate");
|
||||
|
||||
static constexpr remote_base::BitTimings GREE_BIT_TIMINGS{
|
||||
.bit_mark = GREE_BIT_MARK, .one_space = GREE_ONE_SPACE, .zero_space = GREE_ZERO_SPACE};
|
||||
|
||||
void GreeClimate::set_model(Model model) {
|
||||
if (model == GREE_YAN || model == GREE_YX1FF) {
|
||||
// These models only expose vertical swing; horizontal swing is not encoded on the wire.
|
||||
if (model == GREE_YAN) {
|
||||
// YAN only has a vertical vane; the horizontal swing IR bytes are not defined for this model.
|
||||
this->swing_modes_.erase(climate::CLIMATE_SWING_HORIZONTAL);
|
||||
this->swing_modes_.erase(climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
@@ -37,8 +32,6 @@ void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
|
||||
void GreeClimate::transmit_state() {
|
||||
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
|
||||
const Model model = this->model_;
|
||||
// YAW1F uses the YAG byte layout with different framing (see below)
|
||||
const bool yag_layout = model == GREE_YAG || model == GREE_YAW1F;
|
||||
|
||||
remote_state[0] = this->fan_speed_() | this->operation_mode_();
|
||||
remote_state[1] = this->temperature_();
|
||||
@@ -48,35 +41,21 @@ void GreeClimate::transmit_state() {
|
||||
remote_state[4] = this->vertical_swing_();
|
||||
}
|
||||
|
||||
if (model == GREE_YX1FF || yag_layout) {
|
||||
if (model == GREE_YX1FF || model == GREE_YAG) {
|
||||
remote_state[2] = 0x60;
|
||||
remote_state[3] = 0x50;
|
||||
remote_state[4] = this->vertical_swing_();
|
||||
|
||||
if (model == GREE_YX1FF) {
|
||||
// ModelA is set only when powered on.
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF)
|
||||
remote_state[2] &= ~0x40;
|
||||
// YX1FF needs the swing flag as well as its own vane-position value
|
||||
if (this->vertical_swing_() == GREE_VDIR_SWING) {
|
||||
remote_state[0] |= (1 << 6);
|
||||
remote_state[4] = GREE_YX1FF_VANE_SWING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (yag_layout) {
|
||||
remote_state[5] = model == GREE_YAW1F ? 0x80 : 0x40;
|
||||
if (model == GREE_YAG) {
|
||||
remote_state[5] = 0x40;
|
||||
|
||||
if (this->vertical_swing_() == GREE_VDIR_SWING || this->horizontal_swing_() == GREE_HDIR_SWING) {
|
||||
remote_state[0] |= (1 << 6);
|
||||
}
|
||||
}
|
||||
|
||||
if (model == GREE_YAC)
|
||||
remote_state[4] = this->vertical_swing_();
|
||||
|
||||
if (model == GREE_YAC || yag_layout) {
|
||||
if (model == GREE_YAC || model == GREE_YAG) {
|
||||
remote_state[4] |= (this->horizontal_swing_() << 4);
|
||||
}
|
||||
|
||||
@@ -98,7 +77,7 @@ void GreeClimate::transmit_state() {
|
||||
}
|
||||
|
||||
if (model == GREE_YX1FF) {
|
||||
if (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO) == climate::CLIMATE_FAN_HIGH) {
|
||||
if (this->fan_speed_() == GREE_FAN_TURBO) {
|
||||
remote_state[2] |= GREE_FAN_TURBO_BIT;
|
||||
}
|
||||
}
|
||||
@@ -108,7 +87,7 @@ void GreeClimate::transmit_state() {
|
||||
}
|
||||
|
||||
// Calculate the checksum
|
||||
if (model == GREE_YAN) {
|
||||
if (model == GREE_YAN || model == GREE_YX1FF) {
|
||||
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
|
||||
} else {
|
||||
remote_state[7] =
|
||||
@@ -122,32 +101,44 @@ void GreeClimate::transmit_state() {
|
||||
auto *data = transmit.get_data();
|
||||
data->set_carrier_frequency(GREE_IR_FREQUENCY);
|
||||
|
||||
const bool long_gaps = model == GREE_YAC1FB9 || model == GREE_YAW1F;
|
||||
const uint32_t header_space = long_gaps ? GREE_YAC1FB9_HEADER_SPACE : GREE_HEADER_SPACE;
|
||||
const uint32_t message_space = long_gaps ? GREE_YAC1FB9_MESSAGE_SPACE : GREE_MESSAGE_SPACE;
|
||||
auto encode = [&](const uint8_t *state) {
|
||||
data->item(GREE_HEADER_MARK, header_space);
|
||||
data->item_bytes<GREE_BIT_TIMINGS>(state, 4);
|
||||
// Fixed 010 between the two halves, then the message gap
|
||||
data->item(GREE_BIT_MARK, GREE_ZERO_SPACE);
|
||||
data->item(GREE_BIT_MARK, GREE_ONE_SPACE);
|
||||
data->item(GREE_BIT_MARK, GREE_ZERO_SPACE);
|
||||
data->item(GREE_BIT_MARK, message_space);
|
||||
data->item_bytes<GREE_BIT_TIMINGS>(state + 4, 4);
|
||||
data->mark(GREE_BIT_MARK);
|
||||
};
|
||||
|
||||
encode(remote_state);
|
||||
#ifdef USE_GREE_YAW1F
|
||||
if (model == GREE_YAW1F) {
|
||||
// The YAW1F remote follows every command with a second message, 00 00 00 A0 | 00 00 00 A0.
|
||||
// The command buffer is reused so the payload takes no RAM on ESP8266.
|
||||
std::fill_n(remote_state, sizeof(remote_state), 0);
|
||||
remote_state[3] = remote_state[7] = 0xA0;
|
||||
data->space(GREE_YAW1F_FOLLOWUP_SPACE);
|
||||
encode(remote_state);
|
||||
data->mark(GREE_HEADER_MARK);
|
||||
if (model == GREE_YAC1FB9) {
|
||||
data->space(GREE_YAC1FB9_HEADER_SPACE);
|
||||
} else {
|
||||
data->space(GREE_HEADER_SPACE);
|
||||
}
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(GREE_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_ZERO_SPACE);
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_ONE_SPACE);
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(GREE_ZERO_SPACE);
|
||||
|
||||
data->mark(GREE_BIT_MARK);
|
||||
if (model == GREE_YAC1FB9) {
|
||||
data->space(GREE_YAC1FB9_MESSAGE_SPACE);
|
||||
} else {
|
||||
data->space(GREE_MESSAGE_SPACE);
|
||||
}
|
||||
|
||||
for (int i = 4; i < 8; i++) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(GREE_BIT_MARK);
|
||||
bool bit = remote_state[i] & mask;
|
||||
data->space(bit ? GREE_ONE_SPACE : GREE_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
|
||||
data->mark(GREE_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
transmit.perform();
|
||||
@@ -190,9 +181,9 @@ uint8_t GreeClimate::fan_speed_() {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return GREE_FAN_2;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
// HIGH additionally sets the separate Turbo bit in byte 2.
|
||||
return GREE_FAN_3;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return GREE_FAN_TURBO;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
return GREE_FAN_AUTO;
|
||||
|
||||
@@ -40,13 +40,9 @@ static constexpr uint32_t GREE_YAC_HEADER_SPACE = 3000;
|
||||
static constexpr uint32_t GREE_YAC_BIT_MARK = 650;
|
||||
|
||||
// Timing specific to YAC1FB9
|
||||
// Also used by YAW1F
|
||||
static constexpr uint32_t GREE_YAC1FB9_HEADER_SPACE = 4500;
|
||||
static constexpr uint32_t GREE_YAC1FB9_MESSAGE_SPACE = 19980;
|
||||
|
||||
// YAW1F: every command is followed by a second 8-byte message after this gap
|
||||
static constexpr uint32_t GREE_YAW1F_FOLLOWUP_SPACE = 40000;
|
||||
|
||||
// State Frame size
|
||||
static constexpr uint8_t GREE_STATE_FRAME_SIZE = 8;
|
||||
|
||||
@@ -60,8 +56,6 @@ static constexpr uint8_t GREE_VDIR_MUP = 0x03;
|
||||
static constexpr uint8_t GREE_VDIR_MIDDLE = 0x04;
|
||||
static constexpr uint8_t GREE_VDIR_MDOWN = 0x05;
|
||||
static constexpr uint8_t GREE_VDIR_DOWN = 0x06;
|
||||
// YX1FF vane-position byte while swinging
|
||||
static constexpr uint8_t GREE_YX1FF_VANE_SWING = 0x11;
|
||||
|
||||
// Only available on YAC/YAG
|
||||
// Horizontal air directions. Note that these cannot be set on all heat pumps
|
||||
@@ -76,6 +70,7 @@ static constexpr uint8_t GREE_HDIR_RIGHT = 0x06;
|
||||
|
||||
// Only available on YX1FF
|
||||
// Turbo (high) fan mode
|
||||
static constexpr uint8_t GREE_FAN_TURBO = 0x80;
|
||||
static constexpr uint8_t GREE_FAN_TURBO_BIT = 0x10;
|
||||
|
||||
// Available on YX1FF and YAC1FB9
|
||||
@@ -91,7 +86,7 @@ static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
|
||||
static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
|
||||
|
||||
// Model codes
|
||||
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG, GREE_YAW1F };
|
||||
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
|
||||
|
||||
class GreeClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
|
||||
@@ -56,7 +56,6 @@ CONF_HTTP_REQUEST_ID = "http_request_id"
|
||||
CONF_USERAGENT = "useragent"
|
||||
CONF_FOLLOW_REDIRECTS = "follow_redirects"
|
||||
CONF_REDIRECT_LIMIT = "redirect_limit"
|
||||
CONF_MAX_CONCURRENT_REQUESTS = "max_concurrent_requests"
|
||||
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
|
||||
CONF_BUFFER_SIZE_TX = "buffer_size_tx"
|
||||
CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx"
|
||||
@@ -145,10 +144,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.string,
|
||||
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
|
||||
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
|
||||
# Each request holds a client, and a TLS client about 40 kB
|
||||
cv.Optional(CONF_MAX_CONCURRENT_REQUESTS, default=1): cv.int_range(
|
||||
min=1, max=4
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_TIMEOUT, default="4.5s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
@@ -198,8 +193,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_useragent(config[CONF_USERAGENT]))
|
||||
cg.add(var.set_follow_redirects(config[CONF_FOLLOW_REDIRECTS]))
|
||||
cg.add(var.set_redirect_limit(config[CONF_REDIRECT_LIMIT]))
|
||||
if (max_concurrent := config[CONF_MAX_CONCURRENT_REQUESTS]) != 1:
|
||||
cg.add(var.set_max_concurrent_requests(max_concurrent))
|
||||
|
||||
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
|
||||
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
|
||||
@@ -319,19 +312,19 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend(
|
||||
"http_request.get",
|
||||
HttpRequestSendAction,
|
||||
HTTP_REQUEST_GET_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"http_request.post",
|
||||
HttpRequestSendAction,
|
||||
HTTP_REQUEST_POST_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"http_request.send",
|
||||
HttpRequestSendAction,
|
||||
HTTP_REQUEST_SEND_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
synchronous=True,
|
||||
)
|
||||
async def http_request_action_to_code(
|
||||
config: ConfigType,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "http_request.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -16,10 +14,8 @@ void HttpRequestComponent::dump_config() {
|
||||
" Timeout: %" PRIu32 "ms\n"
|
||||
" User-Agent: %s\n"
|
||||
" Follow redirects: %s\n"
|
||||
" Redirect limit: %d\n"
|
||||
" Max concurrent requests: %u",
|
||||
this->timeout_, this->useragent_, YESNO(this->follow_redirects_), this->redirect_limit_,
|
||||
this->max_concurrent_);
|
||||
" Redirect limit: %d",
|
||||
this->timeout_, this->useragent_, YESNO(this->follow_redirects_), this->redirect_limit_);
|
||||
if (this->watchdog_timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_);
|
||||
}
|
||||
@@ -36,209 +32,4 @@ std::string HttpContainer::get_response_header(const std::string &header_name) {
|
||||
return "";
|
||||
}
|
||||
|
||||
void PendingRequest::free_buffer() {
|
||||
if (this->buf != nullptr)
|
||||
RAMAllocator<uint8_t>().deallocate(this->buf, this->buf_size);
|
||||
this->buf = nullptr;
|
||||
}
|
||||
|
||||
void HttpRequestComponent::setup() {
|
||||
// An on_boot automation may already have queued a request
|
||||
if (this->requests_ == nullptr)
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::loop() {
|
||||
if (!this->process_())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::on_safe_shutdown() {
|
||||
// Called before any on_shutdown(). Drop the requests already made, so that they do not hold up the reboot;
|
||||
// only the ones that on_shutdown automations make are sent.
|
||||
this->shutting_down_ = true;
|
||||
this->busy_++;
|
||||
unsigned dropped = 0;
|
||||
for (PendingRequest *request = this->requests_; request != nullptr; request = request->next) {
|
||||
if (request->finished || request->cancelled)
|
||||
continue;
|
||||
dropped++;
|
||||
request->cancelled = true;
|
||||
// As script.stop would, so that a script that was waiting for it can run again from on_shutdown
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_ABANDON);
|
||||
}
|
||||
this->busy_--;
|
||||
if (dropped != 0) {
|
||||
ESP_LOGW(TAG, "Shutting down; %u requests dropped", dropped);
|
||||
}
|
||||
if (this->busy_ == 0)
|
||||
this->remove_dead_();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::on_shutdown() {
|
||||
// Shutdown runs no loop(), so finish the requests here, blocking as they did before. A forced reboot calls
|
||||
// only this, so it also finishes the requests made before it.
|
||||
this->shutting_down_ = true;
|
||||
this->drain_();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::submit(PendingRequest *request) {
|
||||
this->busy_++;
|
||||
uint8_t running = 0;
|
||||
uint8_t waiting = 0;
|
||||
PendingRequest **link = &this->requests_;
|
||||
for (; *link != nullptr; link = &(*link)->next) {
|
||||
const PendingRequest *other = *link;
|
||||
if (other->finished || other->cancelled)
|
||||
continue;
|
||||
if (other->container == nullptr) {
|
||||
waiting++;
|
||||
} else {
|
||||
running++;
|
||||
}
|
||||
}
|
||||
*link = request;
|
||||
this->enable_loop();
|
||||
if (waiting >= MAX_QUEUED) {
|
||||
ESP_LOGW(TAG, "Too many requests waiting; request dropped");
|
||||
request->failed = true;
|
||||
this->finish_(request);
|
||||
} else if (waiting == 0 && running < this->max_concurrent_) {
|
||||
// Start at once; one that cannot start is reported at once
|
||||
this->begin_request_(request);
|
||||
if (request->failed)
|
||||
this->finish_(request);
|
||||
}
|
||||
this->busy_--;
|
||||
if (this->busy_ != 0)
|
||||
return;
|
||||
if (this->shutting_down_) {
|
||||
this->drain_();
|
||||
} else {
|
||||
this->remove_dead_();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpRequestComponent::cancel(void *owner) {
|
||||
for (PendingRequest *request = this->requests_; request != nullptr; request = request->next) {
|
||||
if (request->owner == owner)
|
||||
request->cancelled = true;
|
||||
}
|
||||
if (this->busy_ == 0)
|
||||
this->remove_dead_();
|
||||
}
|
||||
|
||||
bool HttpRequestComponent::process_() {
|
||||
this->busy_++;
|
||||
// The first max_concurrent_ live requests run; a request finished here frees its slot on the next pass
|
||||
uint8_t slots = this->max_concurrent_;
|
||||
for (PendingRequest *request = this->requests_; request != nullptr && slots != 0; request = request->next) {
|
||||
if (request->finished || request->cancelled)
|
||||
continue;
|
||||
slots--;
|
||||
if (request->container == nullptr)
|
||||
this->begin_request_(request);
|
||||
if (!request->cancelled && (request->failed || this->advance_(request)))
|
||||
this->finish_(request);
|
||||
}
|
||||
this->busy_--;
|
||||
if (this->busy_ == 0)
|
||||
this->remove_dead_();
|
||||
return this->requests_ != nullptr;
|
||||
}
|
||||
|
||||
void HttpRequestComponent::drain_() {
|
||||
// A call further up holds request pointers, and runs the new requests itself
|
||||
if (this->busy_ != 0)
|
||||
return;
|
||||
while (this->process_()) {
|
||||
App.feed_wdt();
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpRequestComponent::begin_request_(PendingRequest *request) {
|
||||
request->start_time = millis();
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_START);
|
||||
// Stopped from the request's own lambdas: it is removed without being polled
|
||||
if (!request->cancelled && request->container == nullptr)
|
||||
request->failed = true;
|
||||
}
|
||||
|
||||
bool HttpRequestComponent::advance_(PendingRequest *request) {
|
||||
if (!request->reading) {
|
||||
auto progress = request->container->poll();
|
||||
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING)
|
||||
return false;
|
||||
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED) {
|
||||
request->failed = true;
|
||||
return true;
|
||||
}
|
||||
if (!request->capture)
|
||||
return true;
|
||||
request->buf = RAMAllocator<uint8_t>().allocate(request->buf_size);
|
||||
if (request->buf == nullptr) {
|
||||
// As before, on_response still fires, with an empty body
|
||||
ESP_LOGW(TAG, "Could not allocate %zu bytes for the response body", request->buf_size);
|
||||
return true;
|
||||
}
|
||||
request->reading = true;
|
||||
// millis(), not the loop time: poll() may have blocked earlier in this pass
|
||||
request->last_data_time = millis();
|
||||
}
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// millis(), not the loop time: the budget is shorter than one pass of the loop
|
||||
const uint32_t start = millis();
|
||||
while (request->read_index < request->buf_size) {
|
||||
int read_or_error =
|
||||
request->container->read_available(request->buf + request->read_index, request->buf_size - request->read_index);
|
||||
const uint32_t now = millis();
|
||||
// As before, a body that is cut off still fires on_response with what was read
|
||||
if (read_or_error < 0) {
|
||||
ESP_LOGW(TAG, "Response body read failed after %zu bytes", request->read_index);
|
||||
return true;
|
||||
}
|
||||
if (read_or_error == 0) {
|
||||
if (request->container->is_read_complete())
|
||||
return true;
|
||||
if (now - request->last_data_time < request->timeout)
|
||||
return false;
|
||||
ESP_LOGW(TAG, "Response body timed out after %zu bytes", request->read_index);
|
||||
return true;
|
||||
}
|
||||
request->read_index += read_or_error;
|
||||
request->last_data_time = now;
|
||||
if (now - start >= READ_BUDGET_MS)
|
||||
return false;
|
||||
}
|
||||
// As before, on_response still fires, with the part that fitted
|
||||
if (!request->container->is_read_complete()) {
|
||||
ESP_LOGW(TAG, "Response body is larger than max_response_buffer_size; kept the first %zu bytes", request->buf_size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpRequestComponent::finish_(PendingRequest *request) {
|
||||
request->finished = true;
|
||||
// The body was read over several passes, so time it from the start, as a blocking read would
|
||||
if (request->reading && request->container != nullptr)
|
||||
request->container->duration_ms = millis() - request->start_time;
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_FINISH);
|
||||
}
|
||||
|
||||
void HttpRequestComponent::remove_dead_() {
|
||||
PendingRequest **link = &this->requests_;
|
||||
while (PendingRequest *request = *link) {
|
||||
if (!request->finished && !request->cancelled) {
|
||||
link = &request->next;
|
||||
continue;
|
||||
}
|
||||
*link = request->next;
|
||||
if (request->container != nullptr)
|
||||
request->container->end();
|
||||
request->free_buffer();
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_DESTROY);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::http_request
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -187,13 +186,6 @@ enum class HttpReadLoopResult : uint8_t {
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Progress of a request started with HttpRequestComponent::begin()
|
||||
enum class HttpRequestProgress : uint8_t {
|
||||
HTTP_REQUEST_PROGRESS_PENDING, ///< Still connecting, sending, or waiting for the response headers
|
||||
HTTP_REQUEST_PROGRESS_DONE, ///< Response headers received; status_code is set and the body can be read
|
||||
HTTP_REQUEST_PROGRESS_FAILED, ///< No response; the container must still be ended
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
@@ -257,15 +249,8 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
* - http_read_fully(): for simple "read N bytes" operations
|
||||
*/
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
/// Like read(), but returns 0 at once when no data has arrived yet, where read() may wait for it.
|
||||
virtual int read_available(uint8_t *buf, size_t max_len) { return this->read(buf, max_len); }
|
||||
virtual void end() = 0;
|
||||
|
||||
/// Advance a request started with HttpRequestComponent::begin(); call from the main loop until it is no
|
||||
/// longer pending. Containers from start() are already done. Most calls return quickly, but on ESP-IDF the
|
||||
/// first call for each host waits for the name lookup and the TCP connection, up to the whole timeout.
|
||||
virtual HttpRequestProgress poll() { return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE; }
|
||||
|
||||
void set_secure(bool secure) { this->secure_ = secure; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
@@ -347,56 +332,10 @@ class HttpRequestResponseTrigger final : public Trigger<std::shared_ptr<HttpCont
|
||||
}
|
||||
};
|
||||
|
||||
/// What HttpRequestComponent asks of the action that owns a request
|
||||
enum class RequestEvent : uint8_t {
|
||||
REQUEST_EVENT_START, ///< Build the request and call begin()
|
||||
REQUEST_EVENT_FINISH, ///< Fire the triggers and continue the action chain
|
||||
REQUEST_EVENT_DESTROY, ///< Free the request
|
||||
REQUEST_EVENT_ABANDON, ///< Dropped at shutdown: stop the action, so that it can run again
|
||||
};
|
||||
|
||||
/// A request from an HttpRequestSendAction, run from HttpRequestComponent::loop().
|
||||
/// The action derives from it to add the trigger arguments.
|
||||
struct PendingRequest {
|
||||
using Handler = void (*)(PendingRequest *request, RequestEvent event);
|
||||
PendingRequest(Handler handler, void *owner) : handler(handler), owner(owner) {}
|
||||
PendingRequest(const PendingRequest &) = delete;
|
||||
PendingRequest &operator=(const PendingRequest &) = delete;
|
||||
void free_buffer();
|
||||
|
||||
PendingRequest *next{nullptr};
|
||||
Handler handler;
|
||||
// The action, so that a stop of it can find its requests
|
||||
void *owner;
|
||||
// nullptr until the request has started
|
||||
std::shared_ptr<HttpContainer> container;
|
||||
uint8_t *buf{nullptr};
|
||||
size_t buf_size{0};
|
||||
size_t read_index{0};
|
||||
uint32_t last_data_time{0};
|
||||
uint32_t timeout{0};
|
||||
uint32_t start_time{0};
|
||||
bool capture{false};
|
||||
bool reading{false};
|
||||
bool failed{false};
|
||||
bool finished{false};
|
||||
bool cancelled{false};
|
||||
};
|
||||
|
||||
class HttpRequestComponent : public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void on_safe_shutdown() override;
|
||||
void on_shutdown() override;
|
||||
|
||||
/// For HttpRequestSendAction: queue a request and take ownership of it. Requests start in order, up to
|
||||
/// max_concurrent_requests at a time; one that can start at once is started here.
|
||||
void submit(PendingRequest *request);
|
||||
/// For HttpRequestSendAction: drop the requests of a stopped action without firing their triggers.
|
||||
void cancel(void *owner);
|
||||
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
@@ -410,7 +349,6 @@ class HttpRequestComponent : public Component {
|
||||
}
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
|
||||
void set_max_concurrent_requests(uint8_t max_concurrent) { this->max_concurrent_ = max_concurrent; }
|
||||
|
||||
std::shared_ptr<HttpContainer> get(const char *url) { return this->start(url, "GET", "", std::vector<Header>{}); }
|
||||
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers) {
|
||||
@@ -553,44 +491,16 @@ class HttpRequestComponent : public Component {
|
||||
return this->start(url.c_str(), method.c_str(), body, request_headers, lower_case_collect_headers);
|
||||
}
|
||||
|
||||
/// Start a request without waiting for the response, then poll() the container until it is not pending.
|
||||
/// Returns nullptr when the request cannot be started. Platforms without a non-blocking client complete
|
||||
/// the request here.
|
||||
virtual std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
return this->perform(url, method, body, request_headers, lower_case_collect_headers, timeout_ms);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) = 0;
|
||||
/// Longest time one loop() pass spends reading a response body
|
||||
static constexpr uint32_t READ_BUDGET_MS = 10;
|
||||
/// Requests that wait for a free slot; more are dropped with on_error
|
||||
static constexpr uint8_t MAX_QUEUED = 4;
|
||||
|
||||
bool process_();
|
||||
void drain_();
|
||||
void begin_request_(PendingRequest *request);
|
||||
bool advance_(PendingRequest *request);
|
||||
void finish_(PendingRequest *request);
|
||||
void remove_dead_();
|
||||
|
||||
const char *useragent_{nullptr};
|
||||
bool follow_redirects_{};
|
||||
uint16_t redirect_limit_{};
|
||||
uint32_t timeout_{4500};
|
||||
uint32_t watchdog_timeout_{0};
|
||||
// Oldest first; the first max_concurrent_ live requests run
|
||||
PendingRequest *requests_{nullptr};
|
||||
// Depth of calls that hold request pointers; finished and cancelled requests are freed when it returns to 0
|
||||
uint8_t busy_{0};
|
||||
uint8_t max_concurrent_{1};
|
||||
bool shutting_down_{false};
|
||||
};
|
||||
|
||||
template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...> {
|
||||
@@ -632,42 +542,7 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
this->max_response_buffer_size_ = max_response_buffer_size;
|
||||
}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->parent_->submit(new Request(this, x...)); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override { /* see play_complex */ }
|
||||
|
||||
protected:
|
||||
struct Request final : PendingRequest {
|
||||
Request(HttpRequestSendAction *action, const Ts &...x)
|
||||
: PendingRequest(&HttpRequestSendAction::handle_event, action), args(x...) {}
|
||||
std::tuple<stored_trigger_arg_t<Ts>...> args;
|
||||
};
|
||||
|
||||
static void handle_event(PendingRequest *request, RequestEvent event) {
|
||||
auto *self = static_cast<Request *>(request);
|
||||
auto *action = static_cast<HttpRequestSendAction *>(request->owner);
|
||||
switch (event) {
|
||||
case RequestEvent::REQUEST_EVENT_START:
|
||||
std::apply([action, self](auto &...x) { action->start_request_(*self, x...); }, self->args);
|
||||
break;
|
||||
case RequestEvent::REQUEST_EVENT_FINISH:
|
||||
action->finish_(*self);
|
||||
break;
|
||||
case RequestEvent::REQUEST_EVENT_DESTROY:
|
||||
delete self; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
break;
|
||||
case RequestEvent::REQUEST_EVENT_ABANDON:
|
||||
action->stop_complex();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void stop() override { this->parent_->cancel(this); }
|
||||
|
||||
void start_request_(Request &request, const Ts &...x) {
|
||||
void play(const Ts &...x) override {
|
||||
std::string body;
|
||||
if (this->body_.has_value()) {
|
||||
body = this->body_.value(x...);
|
||||
@@ -684,47 +559,62 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
request_headers.push_back({header.name, header.value(x...)});
|
||||
}
|
||||
|
||||
request.timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
request.capture = this->capture_response_.value(x...);
|
||||
#endif
|
||||
request.buf_size = this->max_response_buffer_size_;
|
||||
request.container = this->parent_->begin(this->url_.value(x...).c_str(), this->method_.value(x...), std::move(body),
|
||||
request_headers, this->lower_case_collect_headers_, request.timeout);
|
||||
}
|
||||
const uint32_t timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
|
||||
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
|
||||
request_headers, this->lower_case_collect_headers_, timeout);
|
||||
|
||||
void finish_(Request &request) {
|
||||
auto &container = request.container;
|
||||
if (request.failed) {
|
||||
std::apply([this](auto &...args) { this->error_trigger_.trigger(args...); }, request.args);
|
||||
auto captured_args = std::make_tuple(x...);
|
||||
|
||||
if (container == nullptr) {
|
||||
std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); },
|
||||
captured_args);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
else if (request.capture) {
|
||||
if (this->capture_response_.value(x...)) {
|
||||
size_t max_length = this->max_response_buffer_size_;
|
||||
std::string response_body;
|
||||
if (request.buf != nullptr)
|
||||
response_body.assign(reinterpret_cast<char *>(request.buf), request.read_index);
|
||||
request.free_buffer();
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = timeout;
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result =
|
||||
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // COMPLETE, ERROR, or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
allocator.deallocate(buf, max_length);
|
||||
}
|
||||
std::apply(
|
||||
[this, &container, &response_body](auto &...args) {
|
||||
this->success_trigger_with_response_.trigger(container, response_body, args...);
|
||||
[this, &container, &response_body](Ts... captured_args_inner) {
|
||||
this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...);
|
||||
},
|
||||
request.args);
|
||||
}
|
||||
captured_args);
|
||||
} else
|
||||
#endif
|
||||
else {
|
||||
std::apply([this, &container](auto &...args) { this->success_trigger_.trigger(container, args...); },
|
||||
request.args);
|
||||
{
|
||||
std::apply([this, &container](
|
||||
Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); },
|
||||
captured_args);
|
||||
}
|
||||
// End it before the chain can start another request, so that one client is held at a time
|
||||
if (container != nullptr) {
|
||||
container->end();
|
||||
container.reset();
|
||||
}
|
||||
// A stop from the triggers ended this run; a run started after it continues the chain instead
|
||||
if (!request.cancelled)
|
||||
std::apply([this](auto &...args) { this->play_next_(args...); }, request.args);
|
||||
container->end();
|
||||
}
|
||||
|
||||
protected:
|
||||
void encode_json_(Ts... x, JsonObject root) {
|
||||
for (const auto &item : this->json_) {
|
||||
auto val = item.second;
|
||||
|
||||
@@ -2,13 +2,8 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
|
||||
@@ -25,10 +20,6 @@
|
||||
namespace esphome::http_request {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "http_request");
|
||||
|
||||
// Request body sent per esp_http_client_write() call, and the time one poll() spends sending it
|
||||
static constexpr size_t SEND_CHUNK_SIZE = 1024;
|
||||
static constexpr uint32_t SEND_BUDGET_MS = 10;
|
||||
static constexpr uint32_t ERROR_DURATION_MS = 1000;
|
||||
|
||||
void HttpRequestIDF::dump_config() {
|
||||
@@ -60,9 +51,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
const char *url, const char *method, const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, [[maybe_unused]] bool async) {
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
|
||||
@@ -114,12 +106,11 @@ std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
config.buffer_size = this->buffer_size_rx_;
|
||||
config.buffer_size_tx = this->buffer_size_tx_;
|
||||
|
||||
const uint32_t start = millis();
|
||||
const uint32_t watchdog_timeout = this->get_watchdog_timeout(timeout_ms);
|
||||
watchdog::WatchdogManager wdm(watchdog_timeout);
|
||||
|
||||
config.event_handler = http_event_handler;
|
||||
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
// Only TLS has a non-blocking connect worth having: the handshake. A plain connection waits for
|
||||
// the TCP connection either way.
|
||||
config.is_async = async && secure;
|
||||
#endif
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (client == nullptr) {
|
||||
@@ -130,9 +121,7 @@ std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
|
||||
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
|
||||
container->set_parent(this);
|
||||
container->set_watchdog_timeout(this->get_watchdog_timeout(timeout_ms));
|
||||
container->timeout_ms_ = timeout_ms;
|
||||
container->async_ = config.is_async;
|
||||
container->set_watchdog_timeout(watchdog_timeout);
|
||||
|
||||
container->set_secure(secure);
|
||||
|
||||
@@ -143,229 +132,101 @@ std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
const int body_len = body.length();
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
std::shared_ptr<HttpContainerIDF> container = this->start_container_(url, method, std::string(body), request_headers,
|
||||
lower_case_collect_headers, timeout_ms, false);
|
||||
if (container == nullptr) {
|
||||
esp_err_t err = esp_http_client_open(client, body_len);
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
// The same stages as begin(), run here until the response headers are in
|
||||
watchdog::WatchdogManager wdm(container->get_watchdog_timeout());
|
||||
HttpRequestProgress progress;
|
||||
while ((progress = container->poll()) == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING) {
|
||||
container->feed_wdt();
|
||||
delay(1);
|
||||
}
|
||||
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED) {
|
||||
container->end();
|
||||
return nullptr;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
return this->start_container_(url, method, std::move(body), request_headers, lower_case_collect_headers, timeout_ms,
|
||||
true);
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainerIDF> HttpRequestIDF::start_container_(
|
||||
const char *url, const char *method, std::string &&body, const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, bool async) {
|
||||
std::shared_ptr<HttpContainerIDF> container =
|
||||
this->create_container_(url, method, request_headers, lower_case_collect_headers, timeout_ms, async);
|
||||
if (container == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
container->body_ = std::move(body);
|
||||
container->start_time_ = millis();
|
||||
container->progress_time_ = container->start_time_;
|
||||
container->redirects_left_ = this->follow_redirects_ ? this->redirect_limit_ : 0;
|
||||
container->stage_ = HttpContainerIDF::Stage::STAGE_CONNECT;
|
||||
return container;
|
||||
}
|
||||
|
||||
HttpRequestIDF *HttpContainerIDF::idf_parent_() { return static_cast<HttpRequestIDF *>(this->parent_); }
|
||||
|
||||
HttpRequestProgress HttpContainerIDF::fail_(esp_err_t err) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
this->parent_->status_momentary_error(ERROR_DURATION_MS);
|
||||
this->stage_ = Stage::STAGE_FAILED;
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
|
||||
}
|
||||
|
||||
HttpRequestProgress HttpContainerIDF::wait_() {
|
||||
// millis(), not the loop time: progress_time_ is set after calls that block within this pass
|
||||
if (millis() - this->progress_time_ >= this->timeout_ms_) {
|
||||
return this->fail_(ESP_ERR_TIMEOUT);
|
||||
}
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
|
||||
bool HttpContainerIDF::socket_ready_(bool write) {
|
||||
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
const int fd = esp_http_client_get_socket(this->client_);
|
||||
if (fd < 0) {
|
||||
return true;
|
||||
}
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
timeval no_wait{};
|
||||
// An error also counts as ready, so that the call that follows reports it
|
||||
// Qualified: esphome::select is a namespace
|
||||
return ::select(fd + 1, write ? nullptr : &fds, write ? &fds : nullptr, nullptr, &no_wait) != 0;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Each stage either finishes, or returns PENDING after a call that did not wait. The waits left are
|
||||
// the name lookup and TCP connection in the first esp_http_client_open() of each hop: a non-blocking
|
||||
// esp-tls connect that times out once never completes, as select() clears the sets it keeps.
|
||||
HttpRequestProgress HttpContainerIDF::poll() {
|
||||
while (true) {
|
||||
switch (this->stage_) {
|
||||
case Stage::STAGE_DONE:
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE;
|
||||
|
||||
case Stage::STAGE_FAILED:
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
|
||||
|
||||
case Stage::STAGE_CONNECT: {
|
||||
// Only the first call of a hop waits
|
||||
watchdog::WatchdogManager wdm(this->connecting_ ? 0 : this->get_watchdog_timeout());
|
||||
esp_http_client_set_timeout_ms(this->client_, this->connecting_ ? 1 : this->timeout_ms_);
|
||||
this->feed_wdt();
|
||||
esp_err_t err = esp_http_client_open(this->client_, this->body_.size());
|
||||
this->feed_wdt();
|
||||
if (err == ESP_ERR_HTTP_EAGAIN) {
|
||||
this->connecting_ = true;
|
||||
return this->wait_();
|
||||
}
|
||||
this->connecting_ = false;
|
||||
if (err != ESP_OK) {
|
||||
return this->fail_(err);
|
||||
}
|
||||
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
if (this->async_) {
|
||||
// Back to a blocking socket, so that mbedtls waits for the rest of a record instead of
|
||||
// failing; reads only start once the socket is readable
|
||||
const int fd = esp_http_client_get_socket(this->client_);
|
||||
const int flags = fd < 0 ? -1 : fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
|
||||
ESP_LOGE(TAG, "Could not make the socket blocking: fd %d, errno %d", fd, errno);
|
||||
return this->fail_(ESP_FAIL);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
|
||||
this->progress_time_ = millis();
|
||||
this->stage_ = this->body_sent_ < this->body_.size() ? Stage::STAGE_SEND_BODY : Stage::STAGE_HEADERS;
|
||||
if (body_len > 0) {
|
||||
int write_left = body_len;
|
||||
int write_index = 0;
|
||||
const char *buf = body.c_str();
|
||||
while (write_left > 0) {
|
||||
int written = esp_http_client_write(client, buf + write_index, write_left);
|
||||
if (written <= 0) {
|
||||
err = ESP_FAIL;
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::STAGE_SEND_BODY: {
|
||||
// esp_http_client_write() returns only once all it was given is sent, so give it a piece at a time
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
const uint32_t start = millis();
|
||||
while (this->body_sent_ < this->body_.size()) {
|
||||
if (!this->socket_ready_(true)) {
|
||||
return this->wait_();
|
||||
}
|
||||
const size_t len = std::min(this->body_.size() - this->body_sent_, SEND_CHUNK_SIZE);
|
||||
int written = esp_http_client_write(this->client_, this->body_.data() + this->body_sent_, len);
|
||||
this->feed_wdt();
|
||||
if (written < 0) {
|
||||
return this->fail_(ESP_FAIL);
|
||||
}
|
||||
if (written == 0) {
|
||||
return this->wait_();
|
||||
}
|
||||
this->body_sent_ += written;
|
||||
this->progress_time_ = millis();
|
||||
if (this->progress_time_ - start >= SEND_BUDGET_MS && this->body_sent_ < this->body_.size()) {
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
}
|
||||
this->body_ = std::string();
|
||||
this->stage_ = Stage::STAGE_HEADERS;
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::STAGE_HEADERS: {
|
||||
if (!this->socket_ready_(false)) {
|
||||
if (millis() - this->progress_time_ < this->timeout_ms_) {
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
// As perform(), where esp_http_client_fetch_headers() times out: no response is status -1
|
||||
return this->response_failed_(-1);
|
||||
}
|
||||
// The response has begun to arrive, so this waits only for the rest of the headers, or over TLS
|
||||
// the rest of the record that holds them
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
this->feed_wdt();
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header)
|
||||
const int64_t length = esp_http_client_fetch_headers(this->client_);
|
||||
this->feed_wdt();
|
||||
this->duration_ms = millis() - this->start_time_;
|
||||
if (length < 0) {
|
||||
return this->response_failed_(-1);
|
||||
}
|
||||
this->content_length = length;
|
||||
this->set_chunked(esp_http_client_is_chunked_response(this->client_));
|
||||
this->status_code = esp_http_client_get_status_code(this->client_);
|
||||
if (is_success(this->status_code)) {
|
||||
this->stage_ = Stage::STAGE_DONE;
|
||||
break;
|
||||
}
|
||||
if (this->redirects_left_ > 0 && is_redirect(this->status_code)) {
|
||||
esp_err_t err = esp_http_client_set_redirection(this->client_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
|
||||
return this->fail_(err);
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char redirect_url[256]{};
|
||||
if (esp_http_client_get_url(this->client_, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
|
||||
ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
|
||||
}
|
||||
#endif
|
||||
this->redirects_left_--;
|
||||
// Collect only the final response's headers; redirects do not resend the body
|
||||
this->response_headers_.clear();
|
||||
this->body_ = std::string();
|
||||
this->body_sent_ = 0;
|
||||
this->progress_time_ = millis();
|
||||
this->stage_ = Stage::STAGE_CONNECT;
|
||||
break;
|
||||
}
|
||||
if (is_redirect(this->status_code) && this->idf_parent_()->follow_redirects_) {
|
||||
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->idf_parent_()->redirect_limit_);
|
||||
}
|
||||
return this->response_failed_(this->status_code);
|
||||
}
|
||||
write_left -= written;
|
||||
write_index += written;
|
||||
container->feed_wdt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequestProgress HttpContainerIDF::response_failed_(int status_code) {
|
||||
this->status_code = status_code;
|
||||
this->duration_ms = millis() - this->start_time_;
|
||||
char url[256]{};
|
||||
esp_http_client_get_url(this->client_, url, sizeof(url) - 1);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, this->status_code);
|
||||
this->parent_->status_momentary_error(ERROR_DURATION_MS);
|
||||
this->stage_ = Stage::STAGE_DONE;
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE;
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
container->feed_wdt();
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
||||
// The read() method handles content_length == 0 specially to support chunked responses.
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
container->duration_ms = millis() - start;
|
||||
if (is_success(container->status_code)) {
|
||||
return container;
|
||||
}
|
||||
|
||||
if (this->follow_redirects_) {
|
||||
auto num_redirects = this->redirect_limit_;
|
||||
while (is_redirect(container->status_code) && num_redirects > 0) {
|
||||
err = esp_http_client_set_redirection(client);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char redirect_url[256]{};
|
||||
if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
|
||||
ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
|
||||
}
|
||||
#endif
|
||||
err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
container->feed_wdt();
|
||||
// IDF is the only backend reusing the container across redirect hops;
|
||||
// drop the previous hop's headers (Arduino/host collect only the final response)
|
||||
container->response_headers_.clear();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
container->duration_ms = millis() - start;
|
||||
if (is_success(container->status_code)) {
|
||||
return container;
|
||||
}
|
||||
|
||||
num_redirects--;
|
||||
}
|
||||
|
||||
if (num_redirects == 0) {
|
||||
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
return container;
|
||||
}
|
||||
|
||||
bool HttpContainerIDF::is_read_complete() const {
|
||||
@@ -404,21 +265,8 @@ bool HttpContainerIDF::is_read_complete() const {
|
||||
// after its internal transport timeout (configured via timeout_ms) expires.
|
||||
// This is passed through as a negative return value, which callers treat as an error.
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
|
||||
return this->read_(buf, max_len);
|
||||
}
|
||||
|
||||
int HttpContainerIDF::read_available(uint8_t *buf, size_t max_len) {
|
||||
// A zero timeout only polls; TLS still waits for the rest of a record that has begun to arrive
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
esp_http_client_set_timeout_ms(this->client_, 0);
|
||||
int read_len_or_error = this->read_(buf, max_len);
|
||||
return read_len_or_error == -ESP_ERR_HTTP_EAGAIN ? 0 : read_len_or_error;
|
||||
}
|
||||
|
||||
int HttpContainerIDF::read_(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content (non-chunked and no-body only).
|
||||
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
|
||||
|
||||
@@ -5,71 +5,30 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_netif.h>
|
||||
#include <esp_tls.h>
|
||||
|
||||
// The socket is needed to wait for the response without blocking; esp_http_client_get_socket() is new in 5.5.3
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 3)
|
||||
#define USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
#endif
|
||||
|
||||
namespace esphome::http_request {
|
||||
|
||||
class HttpRequestIDF;
|
||||
|
||||
class HttpContainerIDF : public HttpContainer {
|
||||
public:
|
||||
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
int read_available(uint8_t *buf, size_t max_len) override;
|
||||
void end() override;
|
||||
bool is_read_complete() const override;
|
||||
HttpRequestProgress poll() override;
|
||||
|
||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||
void feed_wdt();
|
||||
|
||||
protected:
|
||||
friend class HttpRequestIDF;
|
||||
enum class Stage : uint8_t {
|
||||
STAGE_CONNECT,
|
||||
STAGE_SEND_BODY,
|
||||
STAGE_HEADERS,
|
||||
STAGE_DONE,
|
||||
STAGE_FAILED,
|
||||
};
|
||||
|
||||
int read_(uint8_t *buf, size_t max_len);
|
||||
/// True when the socket can be read or written without waiting; always true when it cannot be checked
|
||||
bool socket_ready_(bool write);
|
||||
HttpRequestProgress wait_();
|
||||
HttpRequestProgress fail_(esp_err_t err);
|
||||
/// Finish with an unsuccessful status, which is reported through on_response like a success
|
||||
HttpRequestProgress response_failed_(int status_code);
|
||||
HttpRequestIDF *idf_parent_();
|
||||
|
||||
esp_http_client_handle_t client_;
|
||||
// Owned copy (not a reference): must outlive perform() for the response-header event handler
|
||||
std::vector<std::string> collect_headers_;
|
||||
// Request body for begin(), released once sent
|
||||
std::string body_;
|
||||
size_t body_sent_{0};
|
||||
uint32_t timeout_ms_{0};
|
||||
// Time of the last step forward, for the request timeout of begin()
|
||||
uint32_t progress_time_{0};
|
||||
uint32_t start_time_{0};
|
||||
uint16_t redirects_left_{0};
|
||||
Stage stage_{Stage::STAGE_DONE};
|
||||
// An esp_http_client_open() of a non-blocking client returned ESP_ERR_HTTP_EAGAIN
|
||||
bool connecting_{false};
|
||||
bool async_{false};
|
||||
};
|
||||
|
||||
class HttpRequestIDF final : public HttpRequestComponent {
|
||||
friend class HttpContainerIDF;
|
||||
|
||||
public:
|
||||
// User provided, not "= default": `new(p) HttpRequestIDF()` would zero-fill .bss that is already zero.
|
||||
HttpRequestIDF() {}
|
||||
@@ -81,21 +40,7 @@ class HttpRequestIDF final : public HttpRequestComponent {
|
||||
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
|
||||
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
|
||||
|
||||
std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) override;
|
||||
|
||||
protected:
|
||||
/// A container that poll() takes from the connect through to the response headers
|
||||
std::shared_ptr<HttpContainerIDF> start_container_(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms, bool async);
|
||||
std::shared_ptr<HttpContainerIDF> create_container_(const char *url, const char *method,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms, bool async);
|
||||
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
|
||||
@@ -729,6 +729,7 @@ CONF_DISP_BG_COLOR = "disp_bg_color"
|
||||
CONF_DISP_BG_IMAGE = "disp_bg_image"
|
||||
CONF_DISP_BG_OPA = "disp_bg_opa"
|
||||
CONF_BODY = "body"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_CHANGE_RATE = "change_rate"
|
||||
CONF_CLICKABLE = "clickable"
|
||||
CONF_CLOSE_BUTTON = "close_button"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BUTTONS, CONF_ROWS
|
||||
from esphome.components.const import CONF_ROWS
|
||||
from esphome.components.key_provider import KeyProvider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
|
||||
@@ -10,6 +10,7 @@ from esphome.types import Expression
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
BUTTONMATRIX_CTRLS,
|
||||
CONF_BUTTONS,
|
||||
CONF_CONTROL,
|
||||
CONF_KEY_CODE,
|
||||
CONF_MAIN,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.components.const import CONF_BUTTONS
|
||||
from esphome.const import CONF_BUTTON, CONF_HIDDEN, CONF_ID, CONF_TEXT
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
@@ -8,6 +7,7 @@ from esphome.cpp_types import nullptr
|
||||
from ..defines import (
|
||||
CONF_BODY,
|
||||
CONF_BUTTON_STYLE,
|
||||
CONF_BUTTONS,
|
||||
CONF_CLOSE_BUTTON,
|
||||
CONF_HEADER_BUTTONS,
|
||||
CONF_MAIN,
|
||||
|
||||
@@ -19,7 +19,6 @@ from esphome.const import (
|
||||
CONF_RX_PIN,
|
||||
CONF_TX_BUFFER_SIZE,
|
||||
CONF_TX_PIN,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_USE_ADDRESS,
|
||||
)
|
||||
from esphome.core import CORE, ID, coroutine_with_priority
|
||||
@@ -128,12 +127,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="10min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
# How often the sensor and text_sensor platforms query the modem
|
||||
# Each poll is several AT exchanges, so not more often than every 10 s; the top keeps deadlines in int32_t range
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(seconds=10), max=cv.TimePeriod(days=24)),
|
||||
),
|
||||
**{
|
||||
cv.Optional(a.conf_key): automation.validate_automation({})
|
||||
for a in _CALLBACK_AUTOMATIONS
|
||||
@@ -236,13 +229,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
# The setter only exists with a polling platform; request_polling() calls it then
|
||||
await cg.register_component(
|
||||
var, {k: v for k, v in config.items() if k != CONF_UPDATE_INTERVAL}
|
||||
)
|
||||
|
||||
|
||||
def request_polling(hub: MockObj) -> None:
|
||||
"""Have the modem task query the modem every update_interval for the sensor platforms."""
|
||||
cg.add_define("USE_MODEM_POLL")
|
||||
cg.add(hub.set_update_interval(CORE.config[DOMAIN][CONF_UPDATE_INTERVAL]))
|
||||
await cg.register_component(var, config)
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
@@ -266,16 +265,6 @@ void ModemComponent::loop() {
|
||||
this->wake_task_();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MODEM_POLL
|
||||
if (pending & PENDING_ON_DISCONNECT) {
|
||||
// A lost link must not keep showing the last healthy reading, nor one taken before it dropped
|
||||
this->poll_unpublished_.store(0, std::memory_order_release);
|
||||
this->publish_readings_(CSQ_UNKNOWN, CSQ_UNKNOWN, NETWORK_MODE_NO_SERVICE);
|
||||
} else if (this->poll_unpublished_.load(std::memory_order_acquire)) {
|
||||
this->publish_readings_(this->poll_rssi_, this->poll_ber_, this->poll_mode_);
|
||||
this->poll_unpublished_.store(0, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MODEM_URC
|
||||
if (this->urc_pending_.load(std::memory_order_acquire)) {
|
||||
this->urc_text_sensor_->publish_state(this->urc_line_);
|
||||
@@ -480,16 +469,6 @@ void ModemComponent::dump_config() {
|
||||
this->model_, this->apn_, pin_code, this->tx_pin_->get_pin(), this->rx_pin_->get_pin(),
|
||||
static_cast<int>(this->uart_port_), this->current_baud_rate_, this->tx_buffer_size_,
|
||||
this->rx_buffer_size_, this->dte_buffer_size_, this->timeout_, YESNO(this->is_enabled()));
|
||||
#ifdef USE_MODEM_POLL
|
||||
ESP_LOGCONFIG(TAG, " Update interval: %" PRIu32 " ms", this->update_interval_);
|
||||
#endif
|
||||
#ifdef USE_MODEM_SIGNAL
|
||||
LOG_SENSOR(" ", "RSSI", this->rssi_sensor_);
|
||||
LOG_SENSOR(" ", "BER", this->ber_sensor_);
|
||||
#endif
|
||||
#ifdef USE_MODEM_NETWORK_TYPE
|
||||
LOG_TEXT_SENSOR(" ", "Network type", this->network_type_text_sensor_);
|
||||
#endif
|
||||
this->dump_connect_params_();
|
||||
}
|
||||
|
||||
@@ -508,25 +487,7 @@ void ModemComponent::run_task_() {
|
||||
this->transition_to_(this->compute_next_state_());
|
||||
// Nothing to do while disabled or connected: enable(), disable() and the PPP events wake the task
|
||||
const bool parked = this->component_state_.load() == this->target_state_.load();
|
||||
TickType_t wait = parked ? portMAX_DELAY : pdMS_TO_TICKS(this->step_delay_ms_);
|
||||
#ifdef USE_MODEM_POLL
|
||||
// Like send_at(), the poll goes out between state steps; while connected the next poll ends the wait
|
||||
if (parked && this->is_connected()) {
|
||||
int32_t remaining = static_cast<int32_t>(this->next_poll_ms_ - millis());
|
||||
if (remaining <= 0) {
|
||||
// A result loop() has not published yet is not overwritten; that poll is skipped
|
||||
if (!this->poll_unpublished_.load(std::memory_order_acquire)) {
|
||||
this->poll_();
|
||||
this->poll_unpublished_.store(1, std::memory_order_release);
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
this->next_poll_ms_ = millis() + this->update_interval_;
|
||||
remaining = static_cast<int32_t>(this->update_interval_);
|
||||
}
|
||||
wait = pdMS_TO_TICKS(remaining);
|
||||
}
|
||||
#endif
|
||||
ulTaskNotifyTake(pdTRUE, wait);
|
||||
ulTaskNotifyTake(pdTRUE, parked ? portMAX_DELAY : pdMS_TO_TICKS(this->step_delay_ms_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,9 +862,6 @@ void ModemComponent::transition_to_(ModemState next_state) {
|
||||
}
|
||||
} else if (next_state == ModemState::MODEM_STATE_CONNECTED) {
|
||||
this->hand_over_(PENDING_ON_CONNECT | PENDING_CONNECT_TIMEOUT);
|
||||
#ifdef USE_MODEM_POLL
|
||||
this->next_poll_ms_ = millis(); // the first poll runs right after connecting
|
||||
#endif
|
||||
} else if (next_state == ModemState::MODEM_STATE_DISCONNECTING) {
|
||||
// The lost IP event can lag by minutes; a quick re-enable must wait for a fresh address
|
||||
this->got_ip_.store(0, std::memory_order_release);
|
||||
@@ -1089,54 +1047,6 @@ SimResult ModemComponent::send_pin_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MODEM_POLL
|
||||
|
||||
#ifdef USE_MODEM_SIGNAL
|
||||
// 3GPP TS 45.008 representative bit error rates for the RXQUAL classes AT+CSQ reports
|
||||
static constexpr float BER_PERCENT[] = {0.14f, 0.28f, 0.57f, 1.13f, 2.26f, 4.53f, 9.05f, 18.10f};
|
||||
#endif
|
||||
|
||||
void ModemComponent::poll_() {
|
||||
// The task owns the poll_ answers while poll_unpublished_ is clear
|
||||
#ifdef USE_MODEM_SIGNAL
|
||||
int rssi = CSQ_UNKNOWN, ber = CSQ_UNKNOWN;
|
||||
const bool signal_ok = this->dce_->get_signal_quality(rssi, ber) == command_result::OK;
|
||||
this->poll_rssi_ = static_cast<uint8_t>(signal_ok && rssi >= 0 && rssi < CSQ_UNKNOWN ? rssi : CSQ_UNKNOWN);
|
||||
this->poll_ber_ = static_cast<uint8_t>(signal_ok && ber >= 0 && ber < CSQ_UNKNOWN ? ber : CSQ_UNKNOWN);
|
||||
#endif
|
||||
#ifdef USE_MODEM_NETWORK_TYPE
|
||||
int mode = -1;
|
||||
const bool mode_ok = this->dce_->get_network_system_mode(mode) == command_result::OK;
|
||||
this->poll_mode_ = static_cast<uint8_t>(mode_ok && mode >= 0 && mode < NETWORK_MODE_NONE ? mode : NETWORK_MODE_NONE);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_MODEM_NETWORK_TYPE
|
||||
// AT+CNSMOD values 0-8 on the SIM7600 family; anything else shows as Unknown
|
||||
PROGMEM_STRING_TABLE(NetworkModeStrings, "No service", "GSM", "GPRS", "EDGE", "WCDMA", "HSDPA", "HSUPA", "HSPA", "LTE",
|
||||
"Unknown");
|
||||
#endif
|
||||
|
||||
// Raw AT answers: CSQ rssi and ber, and the CNSMOD mode (past the table shows as Unknown)
|
||||
void ModemComponent::publish_readings_([[maybe_unused]] uint8_t rssi, [[maybe_unused]] uint8_t ber,
|
||||
[[maybe_unused]] uint8_t mode) {
|
||||
#ifdef USE_MODEM_SIGNAL
|
||||
if (this->rssi_sensor_ != nullptr) {
|
||||
this->rssi_sensor_->publish_state(rssi <= CSQ_RSSI_MAX ? -113.0f + 2.0f * rssi : NAN);
|
||||
}
|
||||
if (this->ber_sensor_ != nullptr) {
|
||||
this->ber_sensor_->publish_state(ber < std::size(BER_PERCENT) ? BER_PERCENT[ber] : NAN);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MODEM_NETWORK_TYPE
|
||||
if (this->network_type_text_sensor_ != nullptr) {
|
||||
this->network_type_text_sensor_->publish_state(
|
||||
NetworkModeStrings::get_progmem_str(mode, NetworkModeStrings::LAST_INDEX));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void ModemComponent::dump_connect_params_() {
|
||||
esp_netif_ip_info_t ip;
|
||||
if (!this->got_ip_.load(std::memory_order_acquire) || esp_netif_get_ip_info(this->ppp_netif_, &ip) != ESP_OK) {
|
||||
|
||||
@@ -9,12 +9,9 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/static_task.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#if defined(USE_MODEM_URC) || defined(USE_MODEM_NETWORK_TYPE)
|
||||
#ifdef USE_MODEM_URC
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_MODEM_SIGNAL
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
|
||||
#include <cxx_include/esp_modem_api.hpp>
|
||||
#include <esp_modem_config.h>
|
||||
@@ -134,28 +131,11 @@ class ModemComponent : public Component {
|
||||
network::IPAddresses get_ip_addresses();
|
||||
const char *get_use_address() const { return this->use_address_; }
|
||||
|
||||
#ifdef USE_MODEM_POLL
|
||||
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
|
||||
#endif
|
||||
#ifdef USE_MODEM_SIGNAL
|
||||
SUB_SENSOR(rssi)
|
||||
SUB_SENSOR(ber)
|
||||
#endif
|
||||
#ifdef USE_MODEM_NETWORK_TYPE
|
||||
SUB_TEXT_SENSOR(network_type)
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static constexpr uint32_t COMMAND_TIMEOUT_MS = 1000;
|
||||
static constexpr uint32_t RETRY_DELAY_MS = 1000;
|
||||
static constexpr size_t URC_LINE_SIZE = 128;
|
||||
static constexpr size_t AT_COMMAND_SIZE = 128;
|
||||
#ifdef USE_MODEM_POLL
|
||||
static constexpr uint8_t CSQ_UNKNOWN = 99; // AT+CSQ rssi or ber the modem cannot tell
|
||||
static constexpr uint8_t CSQ_RSSI_MAX = 31;
|
||||
static constexpr uint8_t NETWORK_MODE_NO_SERVICE = 0;
|
||||
static constexpr uint8_t NETWORK_MODE_NONE = 255; // no AT+CNSMOD answer; past the name table, so Unknown
|
||||
#endif
|
||||
|
||||
// esp_modem's calls block until the modem answers or their own timeouts run out, up to a minute for a
|
||||
// mode change, so the state machine runs on its own task and leaves the main loop alone
|
||||
@@ -184,10 +164,6 @@ class ModemComponent : public Component {
|
||||
PENDING_ON_CONNECT = 1 << 5,
|
||||
PENDING_ON_DISCONNECT = 1 << 6,
|
||||
};
|
||||
#ifdef USE_MODEM_POLL
|
||||
void poll_();
|
||||
void publish_readings_(uint8_t rssi, uint8_t ber, uint8_t mode);
|
||||
#endif
|
||||
|
||||
// Main loop only, so arming and cancelling cannot interleave
|
||||
void arm_connect_timeout_();
|
||||
@@ -247,10 +223,6 @@ class ModemComponent : public Component {
|
||||
#endif
|
||||
#ifdef USE_MODEM_SEND_AT
|
||||
uint32_t at_timeout_{COMMAND_TIMEOUT_MS};
|
||||
#endif
|
||||
#ifdef USE_MODEM_POLL
|
||||
uint32_t update_interval_{0};
|
||||
uint32_t next_poll_ms_{0}; // task only
|
||||
#endif
|
||||
uint16_t tx_buffer_size_{1024};
|
||||
uint16_t rx_buffer_size_{1024};
|
||||
@@ -278,10 +250,6 @@ class ModemComponent : public Component {
|
||||
// Set by the UART task with urc_line_; until loop() has published it, new lines are dropped
|
||||
std::atomic<uint8_t> urc_pending_{0};
|
||||
#endif
|
||||
#ifdef USE_MODEM_POLL
|
||||
// Set by the task with the poll_ answers below; until loop() has published them, polls are skipped
|
||||
std::atomic<uint8_t> poll_unpublished_{0};
|
||||
#endif
|
||||
#ifdef USE_MODEM_PIN_CODE
|
||||
std::atomic<PinSave> pin_save_state_{PinSave::PIN_SAVE_DONE};
|
||||
PinStep pin_step_{PinStep::PIN_STEP_IDLE};
|
||||
@@ -291,11 +259,6 @@ class ModemComponent : public Component {
|
||||
uint8_t init_at_count_{0};
|
||||
uint8_t uart_port_{UART_NUM_MAX};
|
||||
uint8_t attach_polls_{0};
|
||||
#ifdef USE_MODEM_POLL
|
||||
uint8_t poll_rssi_{CSQ_UNKNOWN}; // raw AT+CSQ answers
|
||||
uint8_t poll_ber_{CSQ_UNKNOWN};
|
||||
uint8_t poll_mode_{NETWORK_MODE_NONE}; // raw AT+CNSMOD answer
|
||||
#endif
|
||||
bool connect_timeout_armed_{false};
|
||||
bool modem_synced_{false}; // a modem that never answered has nothing to tear down
|
||||
bool resync_{false}; // the attachment read failed, so CONNECTING syncs again first
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_MODEM_ID, ModemComponent, request_polling
|
||||
|
||||
DEPENDENCIES = ["modem"]
|
||||
|
||||
CONF_BER = "ber"
|
||||
CONF_RSSI = "rssi"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_MODEM_ID): cv.use_id(ModemComponent),
|
||||
cv.Optional(CONF_RSSI): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
icon="mdi:signal",
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CONF_RSSI not in config and CONF_BER not in config:
|
||||
return
|
||||
hub = await cg.get_variable(config[CONF_MODEM_ID])
|
||||
request_polling(hub)
|
||||
cg.add_define("USE_MODEM_SIGNAL")
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_RSSI, hub.set_rssi_sensor)
|
||||
await sensors(CONF_BER, hub.set_ber_sensor)
|
||||
@@ -2,51 +2,22 @@ import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODEL, ENTITY_CATEGORY_DIAGNOSTIC
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_MODEM_ID, ModemComponent, request_polling
|
||||
from . import CONF_MODEM_ID, ModemComponent
|
||||
|
||||
DEPENDENCIES = ["modem"]
|
||||
|
||||
CONF_NETWORK_TYPE = "network_type"
|
||||
CONF_URC = "urc"
|
||||
|
||||
# Models whose AT+CNSMOD answers match the table the network type is decoded with
|
||||
NETWORK_TYPE_MODELS = ("SIM7600", "SIM7670")
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_MODEM_ID): cv.use_id(ModemComponent),
|
||||
cv.Optional(CONF_NETWORK_TYPE): text_sensor.text_sensor_schema(
|
||||
icon="mdi:signal-cellular-3", entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||
),
|
||||
cv.Optional(CONF_URC): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_network_type_model(hub: ConfigType) -> ConfigType:
|
||||
if (model := hub[CONF_MODEL]) not in NETWORK_TYPE_MODELS:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_NETWORK_TYPE} is not supported on {model}; supported models: {', '.join(NETWORK_TYPE_MODELS)}",
|
||||
path=[CONF_MODEL],
|
||||
)
|
||||
return hub
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
if CONF_NETWORK_TYPE in config:
|
||||
fv.id_declaration_match_schema(_validate_network_type_model)(
|
||||
config[CONF_MODEM_ID]
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_MODEM_ID])
|
||||
texts = text_sensor.sub_text_sensors(config)
|
||||
@@ -54,6 +25,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_MODEM_URC")
|
||||
cg.add_define("USE_LINE_READER")
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_MODEM_URC_HANDLER", True)
|
||||
if await texts(CONF_NETWORK_TYPE, hub.set_network_type_text_sensor) is not None:
|
||||
cg.add_define("USE_MODEM_NETWORK_TYPE")
|
||||
request_polling(hub)
|
||||
@@ -87,11 +87,6 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import sanitize, snake_case
|
||||
from esphome.mqtt_topics import (
|
||||
DEFAULT_TOPIC_SUFFIXES,
|
||||
per_board_suffix,
|
||||
prefix_gets_mac_suffix,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -271,31 +266,6 @@ def _validate_transport(value: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
# (config key, extra fields when defaulted from the topic prefix, (setter, disabler))
|
||||
_MESSAGES = (
|
||||
(
|
||||
CONF_BIRTH_MESSAGE,
|
||||
{CONF_PAYLOAD: "online"},
|
||||
lambda var: (var.set_birth_message, var.disable_birth_message),
|
||||
),
|
||||
(
|
||||
CONF_WILL_MESSAGE,
|
||||
{CONF_PAYLOAD: "offline"},
|
||||
lambda var: (var.set_last_will, var.disable_last_will),
|
||||
),
|
||||
(
|
||||
CONF_SHUTDOWN_MESSAGE,
|
||||
{CONF_PAYLOAD: "offline"},
|
||||
lambda var: (var.set_shutdown_message, var.disable_shutdown_message),
|
||||
),
|
||||
(
|
||||
CONF_LOG_TOPIC,
|
||||
{},
|
||||
lambda var: (var.set_log_message_template, var.disable_log_message),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def validate_config(value):
|
||||
if (
|
||||
value.get(CONF_TRANSPORT) not in (MQTT_TRANSPORT_WS, MQTT_TRANSPORT_WSS)
|
||||
@@ -310,19 +280,45 @@ def validate_config(value):
|
||||
topic_prefix = value[CONF_TOPIC_PREFIX]
|
||||
# If the topic prefix is not null and these messages are not configured, then set them to the default
|
||||
# If the topic prefix is null and these messages are not configured, then set them to null
|
||||
for key, extra, _ in _MESSAGES:
|
||||
if key in value:
|
||||
continue
|
||||
out[key] = (
|
||||
{}
|
||||
if topic_prefix == ""
|
||||
else {
|
||||
CONF_TOPIC: f"{topic_prefix}{DEFAULT_TOPIC_SUFFIXES[key]}",
|
||||
**extra,
|
||||
if CONF_BIRTH_MESSAGE not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_BIRTH_MESSAGE] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/status",
|
||||
CONF_PAYLOAD: "online",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
out[CONF_BIRTH_MESSAGE] = {}
|
||||
if CONF_WILL_MESSAGE not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_WILL_MESSAGE] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/status",
|
||||
CONF_PAYLOAD: "offline",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
else:
|
||||
out[CONF_WILL_MESSAGE] = {}
|
||||
if CONF_SHUTDOWN_MESSAGE not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_SHUTDOWN_MESSAGE] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/status",
|
||||
CONF_PAYLOAD: "offline",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
else:
|
||||
out[CONF_SHUTDOWN_MESSAGE] = {}
|
||||
if CONF_LOG_TOPIC not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_LOG_TOPIC] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/debug",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
else:
|
||||
out[CONF_LOG_TOPIC] = {}
|
||||
return out
|
||||
|
||||
|
||||
@@ -562,7 +558,7 @@ def _subscriptions(config: ConfigType) -> Iterator[tuple[tuple[str, ...], ...]]:
|
||||
for device in esphome_config.get(CONF_DEVICES, [])
|
||||
}
|
||||
topic_prefix = config[CONF_TOPIC_PREFIX]
|
||||
if prefix_gets_mac_suffix(topic_prefix, CORE.name, mac_suffix):
|
||||
if mac_suffix and topic_prefix == CORE.name:
|
||||
topic_prefix += MAC_SUFFIX
|
||||
# Entities can sit at any depth (a platform can declare its own switches), so find them by mqtt_id
|
||||
for id_, path in iter_ids(full_config):
|
||||
@@ -611,14 +607,12 @@ def _validate_topic_lengths(config: ConfigType) -> ConfigType:
|
||||
FINAL_VALIDATE_SCHEMA = _validate_topic_lengths
|
||||
|
||||
|
||||
def exp_mqtt_message(
|
||||
config: ConfigType | None, topic: cg.MockObj | None = None
|
||||
) -> cg.Expression:
|
||||
def exp_mqtt_message(config):
|
||||
if config is None:
|
||||
return cg.optional(cg.TemplateArguments(MQTTMessage))
|
||||
return cg.StructInitializer(
|
||||
MQTTMessage,
|
||||
("topic", config[CONF_TOPIC] if topic is None else topic),
|
||||
("topic", config[CONF_TOPIC]),
|
||||
("payload", config.get(CONF_PAYLOAD, "")),
|
||||
("qos", config[CONF_QOS]),
|
||||
("retain", config[CONF_RETAIN]),
|
||||
@@ -702,33 +696,37 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
|
||||
topic_prefix = config[CONF_TOPIC_PREFIX]
|
||||
# The MAC suffix is only known at runtime, so the prefix and the topics defaulted from it
|
||||
# are built from App.get_name(); topics written out by hand stay literal
|
||||
mac_suffix = CORE.config[CONF_ESPHOME].get(CONF_NAME_ADD_MAC_SUFFIX, False)
|
||||
mac_prefix = prefix_gets_mac_suffix(topic_prefix, CORE.name, mac_suffix)
|
||||
cg.add(var.set_topic_prefix(cg.App.get_name() if mac_prefix else topic_prefix))
|
||||
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name))
|
||||
|
||||
if config[CONF_USE_ABBREVIATIONS]:
|
||||
cg.add_define("USE_MQTT_ABBREVIATIONS")
|
||||
|
||||
for key, _, methods in _MESSAGES:
|
||||
set_message, disable_message = methods(var)
|
||||
if not (message := config[key]):
|
||||
cg.add(disable_message())
|
||||
continue
|
||||
topic = (
|
||||
cg.App.get_name() + suffix
|
||||
if (suffix := per_board_suffix(config, key, CORE.name, mac_suffix))
|
||||
else None
|
||||
)
|
||||
cg.add(set_message(exp_mqtt_message(message, topic)))
|
||||
birth_message = config[CONF_BIRTH_MESSAGE]
|
||||
if not birth_message:
|
||||
cg.add(var.disable_birth_message())
|
||||
else:
|
||||
cg.add(var.set_birth_message(exp_mqtt_message(birth_message)))
|
||||
will_message = config[CONF_WILL_MESSAGE]
|
||||
if not will_message:
|
||||
cg.add(var.disable_last_will())
|
||||
else:
|
||||
cg.add(var.set_last_will(exp_mqtt_message(will_message)))
|
||||
shutdown_message = config[CONF_SHUTDOWN_MESSAGE]
|
||||
if not shutdown_message:
|
||||
cg.add(var.disable_shutdown_message())
|
||||
else:
|
||||
cg.add(var.set_shutdown_message(exp_mqtt_message(shutdown_message)))
|
||||
|
||||
if log_topic := config[CONF_LOG_TOPIC]:
|
||||
log_topic = config[CONF_LOG_TOPIC]
|
||||
if not log_topic:
|
||||
cg.add(var.disable_log_message())
|
||||
else:
|
||||
cg.add(var.set_log_message_template(exp_mqtt_message(log_topic)))
|
||||
# Request a log listener slot only when log topic is enabled
|
||||
logger.request_log_listener()
|
||||
if (level := log_topic.get(CONF_LEVEL)) is not None:
|
||||
cg.add(var.set_log_level(logger.LOG_LEVELS[level]))
|
||||
|
||||
if CONF_LEVEL in log_topic:
|
||||
cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]]))
|
||||
|
||||
cg.add(var.set_keep_alive(config[CONF_KEEPALIVE]))
|
||||
|
||||
|
||||
@@ -737,7 +737,14 @@ void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { th
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
|
||||
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
|
||||
void MQTTClientComponent::set_topic_prefix(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); }
|
||||
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) {
|
||||
if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) {
|
||||
char buf[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
|
||||
this->topic_prefix_ = str_sanitize_to(buf, App.get_name().c_str());
|
||||
} else {
|
||||
this->topic_prefix_ = topic_prefix;
|
||||
}
|
||||
}
|
||||
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||
void MQTTClientComponent::disable_birth_message() {
|
||||
this->birth_message_.topic = "";
|
||||
|
||||
@@ -201,7 +201,7 @@ class MQTTClientComponent final : public Component {
|
||||
*
|
||||
* @param topic_prefix The topic prefix. The last "/" is appended automatically.
|
||||
*/
|
||||
void set_topic_prefix(std::string topic_prefix);
|
||||
void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix);
|
||||
/// Get the topic prefix of this device, using default if necessary
|
||||
const std::string &get_topic_prefix() const;
|
||||
|
||||
|
||||
@@ -231,47 +231,30 @@ void OnlineImage::loop() {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else {
|
||||
// Nothing new arrived. The decoder works for a limited time per call, so it may still have data to use.
|
||||
if (this->download_buffer_.unread() != 0) {
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (consumed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", esphome::runtime_image::decode_error_to_string(consumed));
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
} else if (this->downloader_->is_read_complete()) {
|
||||
// The response has ended, so data left in the buffer can never be completed
|
||||
size_t unread = this->download_buffer_.unread();
|
||||
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
|
||||
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
|
||||
if (this->is_decode_finished() || !this->decoder_knows_end()) {
|
||||
this->finish_download_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
} else if (unread == 0) {
|
||||
ESP_LOGE(TAG, "Server sent an empty response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
|
||||
}
|
||||
if (this->downloader_->is_read_complete()) {
|
||||
// The response has ended, so data left in the buffer can never be completed
|
||||
size_t unread = this->download_buffer_.unread();
|
||||
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
|
||||
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
|
||||
if (this->is_decode_finished() || !this->decoder_knows_end()) {
|
||||
this->finish_download_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
} else if (unread == 0) {
|
||||
ESP_LOGE(TAG, "Server sent an empty response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
// ESP-IDF keeps returning 0 after the server closes the connection early
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
// ESP-IDF keeps returning 0 after the server closes the connection early
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Buffer is full, need to decode some data first
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import wifi
|
||||
from esphome.components.const import CONF_BORDER_ROUTER, CONF_DEVICE_TYPE
|
||||
from esphome.components.const import CONF_BORDER_ROUTER
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
@@ -74,6 +74,7 @@ from esphome.writer import clean_build
|
||||
|
||||
from .const import (
|
||||
CONF_CORE_OPTIONS,
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_EXT_PAN_ID,
|
||||
CONF_FORCE_DATASET,
|
||||
CONF_MDNS_ID,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
CONF_CORE_OPTIONS = "core_options"
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
CONF_EXT_PAN_ID = "ext_pan_id"
|
||||
CONF_FORCE_DATASET = "force_dataset"
|
||||
CONF_MDNS_ID = "mdns_id"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
@@ -9,27 +7,14 @@ from esphome.const import CONF_ID
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import layers
|
||||
from .layers import (
|
||||
PacketInterface,
|
||||
build_stack,
|
||||
check_session,
|
||||
packet_interface_ns,
|
||||
stack_auto_load,
|
||||
stack_schema,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DOMAIN = "packet_interface"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
CONF_PACKET_INTERFACE = "packet_interface"
|
||||
|
||||
# Each module in layers registers a layer type; import them all before building the schemas from the registry
|
||||
for module_info in pkgutil.iter_modules(layers.__path__):
|
||||
importlib.import_module(f".layers.{module_info.name}", package=__package__)
|
||||
|
||||
LINK_LAYER_SCHEMAS = stack_schema()
|
||||
packet_interface_ns = cg.esphome_ns.namespace("packet_interface")
|
||||
PacketInterface = packet_interface_ns.class_("PacketInterface", cg.Component)
|
||||
|
||||
__all__ = [
|
||||
"CONF_MAX_PACKET_SIZE",
|
||||
@@ -41,47 +26,24 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def AUTO_LOAD(config: list[ConfigType] | None = None) -> list[str]:
|
||||
"""Load only what the configured layers use.
|
||||
|
||||
Without a config, as tooling that lists dependencies calls it, everything a layer could load.
|
||||
"""
|
||||
if config is None:
|
||||
return ["binary_sensor", "xxtea"]
|
||||
loads: set[str] = set()
|
||||
for interface in config:
|
||||
loads |= stack_auto_load(interface)
|
||||
return sorted(loads)
|
||||
|
||||
|
||||
def _validate_layers(config: ConfigType) -> ConfigType:
|
||||
check_session(config)
|
||||
return config
|
||||
|
||||
|
||||
def packet_interface_schema(
|
||||
class_: MockObjClass,
|
||||
default_max_packet_size: int,
|
||||
max_packet_size_limit: int = 4096,
|
||||
) -> cv.Schema:
|
||||
"""Schema for a link platform, with the layers that can be stacked on it."""
|
||||
return (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.Optional(
|
||||
CONF_MAX_PACKET_SIZE, default=default_max_packet_size
|
||||
): cv.int_range(16, max_packet_size_limit),
|
||||
**LINK_LAYER_SCHEMAS,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.add_extra(_validate_layers)
|
||||
)
|
||||
"""Schema for a link platform."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.Optional(
|
||||
CONF_MAX_PACKET_SIZE, default=default_max_packet_size
|
||||
): cv.int_range(16, max_packet_size_limit),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def new_packet_interface(config: ConfigType, *args: Any) -> MockObj:
|
||||
"""Create the link layer and the layers configured on it, from the bottom up.
|
||||
"""Create the link layer.
|
||||
|
||||
The link's constructor takes its maximum packet size, usually as a std::span over a buffer that code generation
|
||||
sized from the configuration, so the two cannot differ.
|
||||
@@ -91,5 +53,4 @@ async def new_packet_interface(config: ConfigType, *args: Any) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
cg.add(var.set_name(config[CONF_ID].id))
|
||||
await cg.register_component(var, config)
|
||||
await build_stack(config, var)
|
||||
return var
|
||||
@@ -1,135 +0,0 @@
|
||||
#include "basic_session.h"
|
||||
#ifdef USE_PACKET_INTERFACE_BASIC_SESSION
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.session");
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_DATA = 0x00;
|
||||
static constexpr uint8_t PACKET_TYPE_KEEP_ALIVE = 0x01;
|
||||
// Type byte and the 32-bit boot id, little endian
|
||||
static constexpr size_t KEEP_ALIVE_SIZE = 5;
|
||||
|
||||
BasicSession::BasicSession(PacketInterface *lower)
|
||||
: PacketInterface(remaining_packet_size(lower->get_max_packet_size(), HEADER_SIZE)), lower_(lower) {
|
||||
this->stack_on_(lower, CAPABILITY_SESSION);
|
||||
// Data lost by a reliability layer below must be sent again, as after a restart of the peer
|
||||
if (lower->has_capability(CAPABILITY_RELIABILITY)) {
|
||||
lower->add_on_session_state_callback([this](bool connected) {
|
||||
if (!connected && this->connected_)
|
||||
this->set_connected_(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void BasicSession::setup() {
|
||||
if (this->lower_->get_max_packet_size() < KEEP_ALIVE_SIZE) {
|
||||
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
|
||||
return;
|
||||
}
|
||||
this->boot_id_ = random_uint32();
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr)
|
||||
this->connected_sensor_->publish_initial_state(false);
|
||||
#endif
|
||||
this->lower_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
|
||||
// A reliability layer below queues this until its link is up, so the peer hears of a restart straight away
|
||||
this->send_keep_alive_();
|
||||
this->set_interval(this->keep_alive_ms_, [this] { this->on_interval_(); });
|
||||
}
|
||||
|
||||
void BasicSession::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Basic Session on '%s':\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Keep Alive: %" PRIu32 "ms\n"
|
||||
" Timeout: %" PRIu32 "ms",
|
||||
this->name_, this->max_packet_size_, this->keep_alive_ms_, this->timeout_ms_);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool BasicSession::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
uint8_t header = PACKET_TYPE_DATA;
|
||||
PacketBuffer packet(&header, HEADER_SIZE, &data, nullptr, 0);
|
||||
return this->lower_->send_to_interface(packet, meta_data);
|
||||
}
|
||||
|
||||
void BasicSession::send_keep_alive_() {
|
||||
uint8_t packet[KEEP_ALIVE_SIZE] = {
|
||||
PACKET_TYPE_KEEP_ALIVE, static_cast<uint8_t>(this->boot_id_), static_cast<uint8_t>(this->boot_id_ >> 8),
|
||||
static_cast<uint8_t>(this->boot_id_ >> 16), static_cast<uint8_t>(this->boot_id_ >> 24)};
|
||||
if (!this->lower_->send_to_interface(PacketBuffer(packet, sizeof(packet)), {}))
|
||||
ESP_LOGV(TAG, "%s: Lower layer did not accept keep-alive", this->name_);
|
||||
}
|
||||
|
||||
void BasicSession::on_interval_() {
|
||||
this->send_keep_alive_();
|
||||
if (this->connected_ && App.get_loop_component_start_time() - this->last_receive_ms_ > this->timeout_ms_) {
|
||||
ESP_LOGW(TAG, "%s: Peer not heard for %" PRIu32 "ms", this->name_, this->timeout_ms_);
|
||||
this->set_connected_(false);
|
||||
}
|
||||
}
|
||||
|
||||
void BasicSession::set_connected_(bool connected) {
|
||||
this->connected_ = connected;
|
||||
ESP_LOGD(TAG, "%s: Session %s", this->name_, connected ? LOG_STR_LITERAL("connected") : LOG_STR_LITERAL("lost"));
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr)
|
||||
this->connected_sensor_->publish_state(connected);
|
||||
#endif
|
||||
this->session_callback_.call(connected);
|
||||
}
|
||||
|
||||
void BasicSession::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
// Lower layers deliver packets in one piece; the payload is passed up without copying
|
||||
if (!data.is_contiguous()) {
|
||||
ESP_LOGW(TAG, "%s: Packet split across buffers is not supported", this->name_);
|
||||
return;
|
||||
}
|
||||
auto packet = data.as_span();
|
||||
if (packet.empty()) {
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
uint8_t type = packet[0];
|
||||
if (type == PACKET_TYPE_KEEP_ALIVE) {
|
||||
if (packet.size() != KEEP_ALIVE_SIZE) {
|
||||
ESP_LOGV(TAG, "%s: Keep-alive of %zu bytes dropped", this->name_, packet.size());
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
uint32_t boot_id = encode_uint32(packet[4], packet[3], packet[2], packet[1]);
|
||||
if (!this->peer_known_ || boot_id != this->peer_boot_id_) {
|
||||
// Data may arrive before the first keep-alive, so only a changed id means a restart
|
||||
if (this->connected_ && this->peer_known_) {
|
||||
ESP_LOGD(TAG, "%s: Peer has restarted", this->name_);
|
||||
this->set_connected_(false);
|
||||
}
|
||||
this->peer_boot_id_ = boot_id;
|
||||
this->peer_known_ = true;
|
||||
// Answer a new peer at once; it answers only if our id is new to it, so this cannot repeat
|
||||
this->send_keep_alive_();
|
||||
}
|
||||
} else if (type != PACKET_TYPE_DATA) {
|
||||
ESP_LOGV(TAG, "%s: Unknown packet type 0x%02X dropped", this->name_, type);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
this->note_received_(true);
|
||||
this->last_receive_ms_ = App.get_loop_component_start_time();
|
||||
if (!this->connected_)
|
||||
this->set_connected_(true);
|
||||
if (type == PACKET_TYPE_DATA)
|
||||
this->on_receive_from_interface_(PacketBuffer(packet.data() + HEADER_SIZE, packet.size() - HEADER_SIZE), meta_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_PACKET_INTERFACE_BASIC_SESSION
|
||||
#include "packet_interface.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A session layer that reports whether the peer can be reached.
|
||||
*
|
||||
* Every packet starts with a type byte. A data packet carries the payload of the layer above. A keep-alive packet
|
||||
* carries a random number chosen at boot, so that a restart of the peer is noticed even if it is quicker than the
|
||||
* timeout. A keep-alive is sent every keep_alive interval, and at once when a new peer is heard. The peer is
|
||||
* considered gone when nothing has been received from it for the timeout; this is checked every keep_alive interval.
|
||||
* A restart of the peer is reported as a disconnect followed by a connect. So is a reset of a reliability layer below,
|
||||
* which discards data: the session is reported lost, and connected again when the peer is next heard.
|
||||
*/
|
||||
class BasicSession : public PacketInterface {
|
||||
public:
|
||||
/// The packet type byte.
|
||||
static constexpr size_t HEADER_SIZE = 1;
|
||||
|
||||
explicit BasicSession(PacketInterface *lower);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override;
|
||||
|
||||
void set_keep_alive(uint32_t keep_alive_ms) { this->keep_alive_ms_ = keep_alive_ms; }
|
||||
void set_session_timeout(uint32_t timeout_ms) { this->timeout_ms_ = timeout_ms; }
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; }
|
||||
#endif
|
||||
|
||||
bool is_connected() const { return this->connected_; }
|
||||
|
||||
protected:
|
||||
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
void on_interval_();
|
||||
void send_keep_alive_();
|
||||
void set_connected_(bool connected);
|
||||
|
||||
PacketInterface *lower_;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *connected_sensor_{nullptr};
|
||||
#endif
|
||||
uint32_t keep_alive_ms_{10000};
|
||||
uint32_t timeout_ms_{30000};
|
||||
uint32_t last_receive_ms_{0};
|
||||
uint32_t boot_id_{0};
|
||||
uint32_t peer_boot_id_{0};
|
||||
bool peer_known_{false};
|
||||
bool connected_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,365 +0,0 @@
|
||||
#include "hdlc_reliability.h"
|
||||
#ifdef USE_PACKET_INTERFACE_HDLC
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.hdlc");
|
||||
|
||||
static constexpr uint32_t RETRANSMIT_TIMER_ID = 0;
|
||||
static constexpr uint32_t ACK_TIMER_ID = 1;
|
||||
static constexpr uint32_t SEND_RETRY_TIMER_ID = 2;
|
||||
// A frame or acknowledgement the lower layer refused, for example while a radio was receiving, is sent again after this
|
||||
static constexpr uint32_t SEND_RETRY_MS = 50;
|
||||
// SABM is repeated at this interval, or the timeout if longer, until the peer answers
|
||||
static constexpr uint32_t RESET_INTERVAL_MS = 1000;
|
||||
|
||||
// Control byte formats, with the poll/final bit (0x10) ignored
|
||||
static constexpr uint8_t POLL_FINAL_BIT = 0x10;
|
||||
static constexpr uint8_t SUPERVISORY_FRAME = 0x01;
|
||||
static constexpr uint8_t SUPERVISORY_RR = 0x00;
|
||||
static constexpr uint8_t SUPERVISORY_REJ = 0x02;
|
||||
static constexpr uint8_t UNNUMBERED_SABM = 0x2F;
|
||||
static constexpr uint8_t UNNUMBERED_UA = 0x63;
|
||||
|
||||
static uint16_t frame_crc(uint8_t control, const uint8_t *payload, size_t length) {
|
||||
uint16_t crc = crc16(&control, 1);
|
||||
return crc16(payload, length, crc);
|
||||
}
|
||||
|
||||
HdlcReliability::HdlcReliability(PacketInterface *lower)
|
||||
: PacketInterface(remaining_packet_size(lower->get_max_packet_size(), FRAME_OVERHEAD)), lower_(lower) {
|
||||
this->stack_on_(lower, CAPABILITY_RELIABILITY);
|
||||
}
|
||||
|
||||
void HdlcReliability::setup() {
|
||||
if (this->max_packet_size_ == 0) {
|
||||
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
|
||||
return;
|
||||
}
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->tx_slots_ = allocator.allocate(size_t{this->max_queue_length_} * this->max_packet_size_);
|
||||
if (this->tx_slots_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Not enough memory for the send queue"));
|
||||
return;
|
||||
}
|
||||
this->lower_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
|
||||
}
|
||||
|
||||
void HdlcReliability::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"HDLC Reliability on '%s':\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Timeout: %" PRIu32 "ms\n"
|
||||
" Ack Delay: %" PRIu32 "ms\n"
|
||||
" Max Queue Length: %u\n"
|
||||
" Max Retries: %u",
|
||||
this->name_, this->max_packet_size_, this->timeout_ms_, this->ack_delay_ms_, this->max_queue_length_,
|
||||
this->max_retries_);
|
||||
}
|
||||
|
||||
void HdlcReliability::loop() {
|
||||
// Disabled first, as sending may deliver a reply to this layer and request the loop again
|
||||
this->disable_loop();
|
||||
if (this->state_ == LinkState::LINK_STATE_RESET_PENDING) {
|
||||
this->state_ = LinkState::LINK_STATE_RESETTING;
|
||||
this->start_timer_();
|
||||
this->send_frame_(UNNUMBERED_SABM, nullptr, 0);
|
||||
}
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED)
|
||||
return;
|
||||
// Queued frames go first, as each carries the acknowledgement
|
||||
if (this->send_pending_() && this->ack_due_ && !this->send_supervisory_(SUPERVISORY_RR))
|
||||
this->retry_send_();
|
||||
}
|
||||
|
||||
void HdlcReliability::retry_send_() {
|
||||
this->set_timeout(SEND_RETRY_TIMER_ID, SEND_RETRY_MS, [this]() { this->request_loop_(); });
|
||||
}
|
||||
|
||||
void HdlcReliability::owe_ack_(bool now) {
|
||||
if (now || this->ack_delay_ms_ == 0) {
|
||||
this->ack_due_ = true;
|
||||
this->request_loop_();
|
||||
} else if (!this->ack_pending_) {
|
||||
// Waiting lets one acknowledgement cover several frames, or a reply carry it
|
||||
this->set_timeout(ACK_TIMER_ID, this->ack_delay_ms_, [this]() {
|
||||
this->ack_due_ = true;
|
||||
this->request_loop_();
|
||||
});
|
||||
}
|
||||
this->ack_pending_ = true;
|
||||
}
|
||||
|
||||
void HdlcReliability::ack_sent_() {
|
||||
this->ack_pending_ = false;
|
||||
this->ack_due_ = false;
|
||||
this->cancel_timeout(ACK_TIMER_ID);
|
||||
}
|
||||
|
||||
void HdlcReliability::request_loop_() { this->enable_loop(); }
|
||||
|
||||
bool HdlcReliability::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
if (this->tx_slots_ == nullptr)
|
||||
return false;
|
||||
size_t size = data.size();
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
uint8_t outstanding = this->outstanding_();
|
||||
if (outstanding >= this->max_queue_length_) {
|
||||
if (this->state_ == LinkState::LINK_STATE_CONNECTED) {
|
||||
ESP_LOGV(TAG, "%s: Send queue full", this->name_);
|
||||
} else if (!this->reset_wait_logged_) {
|
||||
// Once for each reset; the layers above keep trying while the peer is away
|
||||
this->reset_wait_logged_ = true;
|
||||
ESP_LOGD(TAG, "%s: Send queue full, waiting for the peer to answer the link reset", this->name_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
uint8_t seq = this->vs_;
|
||||
data.copy_to(this->slot_(seq), size);
|
||||
this->tx_lengths_[this->slot_index_(seq)] = size;
|
||||
this->vs_ = (seq + 1) & 7;
|
||||
// While the link is being reset, the frame waits in the queue and is sent when UA arrives
|
||||
if (this->state_ == LinkState::LINK_STATE_CONNECTED) {
|
||||
this->send_pending_();
|
||||
if (outstanding == 0)
|
||||
this->start_timer_();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HdlcReliability::send_frame_(uint8_t control, const uint8_t *payload, size_t length) {
|
||||
uint16_t crc = frame_crc(control, payload, length);
|
||||
uint8_t trailer[2] = {static_cast<uint8_t>(crc), static_cast<uint8_t>(crc >> 8)};
|
||||
PacketBuffer body(payload, length);
|
||||
PacketBuffer frame(&control, 1, length != 0 ? &body : nullptr, trailer, sizeof(trailer));
|
||||
if (this->lower_->send_to_interface(frame, {}))
|
||||
return true;
|
||||
ESP_LOGV(TAG, "%s: Lower layer did not accept frame 0x%02X", this->name_, control);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HdlcReliability::send_supervisory_(uint8_t type) {
|
||||
if (!this->send_frame_(SUPERVISORY_FRAME | (type << 2) | (this->vr_ << 5), nullptr, 0))
|
||||
return false;
|
||||
this->ack_sent_();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sends the next queued frame not yet sent, and returns true if none are left. One frame per loop pass lets other
|
||||
// components run between frames: on a radio, sending blocks, and the peer's frames are received in between. If the
|
||||
// lower layer refuses the frame, it is sent again shortly.
|
||||
bool HdlcReliability::send_pending_() {
|
||||
if (this->vn_ == this->vs_)
|
||||
return true;
|
||||
uint8_t control = (this->vn_ << 1) | (this->vr_ << 5);
|
||||
if (!this->send_frame_(control, this->slot_(this->vn_), this->tx_lengths_[this->slot_index_(this->vn_)])) {
|
||||
this->retry_send_();
|
||||
return false;
|
||||
}
|
||||
// Every I-frame carries an acknowledgement, so a pending RR is no longer needed
|
||||
this->ack_sent_();
|
||||
this->vn_ = (this->vn_ + 1) & 7;
|
||||
if (this->vn_ == this->vs_)
|
||||
return true;
|
||||
this->request_loop_();
|
||||
return false;
|
||||
}
|
||||
|
||||
void HdlcReliability::resend_outstanding_() {
|
||||
this->vn_ = this->va_;
|
||||
this->send_pending_();
|
||||
}
|
||||
|
||||
void HdlcReliability::start_timer_() {
|
||||
uint32_t interval = this->timeout_ms_;
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED && interval < RESET_INTERVAL_MS)
|
||||
interval = RESET_INTERVAL_MS;
|
||||
// Up to a quarter longer at random, so devices whose frames collided on a shared channel do not resend together
|
||||
interval += random_uint32() % (interval / 4 + 1);
|
||||
this->set_timeout(RETRANSMIT_TIMER_ID, interval, [this]() { this->on_timeout_(); });
|
||||
}
|
||||
|
||||
void HdlcReliability::on_timeout_() {
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED) {
|
||||
// Counted up to max_retries_ only, so the warning is logged once until the link comes up
|
||||
if (this->retries_ < this->max_retries_) {
|
||||
this->retries_++;
|
||||
if (this->retries_ == this->max_retries_) {
|
||||
ESP_LOGW(TAG, "%s: No answer to the link reset after %u tries; check that the peer has a reliability layer",
|
||||
this->name_, this->max_retries_);
|
||||
}
|
||||
}
|
||||
this->send_frame_(UNNUMBERED_SABM, nullptr, 0);
|
||||
this->start_timer_();
|
||||
return;
|
||||
}
|
||||
if (this->outstanding_() == 0)
|
||||
return;
|
||||
if (this->retries_ == this->max_retries_) {
|
||||
ESP_LOGW(TAG, "%s: No acknowledgement after %u retries, resetting link", this->name_, this->max_retries_);
|
||||
this->start_reset_();
|
||||
return;
|
||||
}
|
||||
this->retries_++;
|
||||
ESP_LOGV(TAG, "%s: Timeout, resending %u frames", this->name_, this->outstanding_());
|
||||
this->resend_outstanding_();
|
||||
this->start_timer_();
|
||||
}
|
||||
|
||||
// Unacknowledged frames are discarded; frames queued from now on are numbered from 0 and sent once the link is up.
|
||||
// Resetting a connected link can lose data both ways, which is reported to the layers above.
|
||||
void HdlcReliability::start_reset_() {
|
||||
bool was_connected = this->state_ == LinkState::LINK_STATE_CONNECTED;
|
||||
this->state_ = LinkState::LINK_STATE_RESET_PENDING;
|
||||
this->va_ = this->vs_ = this->vn_ = 0;
|
||||
this->head_ = 0;
|
||||
this->vr_ = 0;
|
||||
this->retries_ = 0;
|
||||
this->rej_sent_ = false;
|
||||
this->ack_sent_();
|
||||
this->cancel_timeout(RETRANSMIT_TIMER_ID);
|
||||
this->request_loop_();
|
||||
if (was_connected)
|
||||
this->session_callback_.call(false);
|
||||
}
|
||||
|
||||
void HdlcReliability::link_up_() {
|
||||
ESP_LOGD(TAG, "%s: Link connected", this->name_);
|
||||
this->state_ = LinkState::LINK_STATE_CONNECTED;
|
||||
this->reset_wait_logged_ = false;
|
||||
this->peer_frame_seen_ = false;
|
||||
this->retries_ = 0;
|
||||
this->cancel_timeout(RETRANSMIT_TIMER_ID);
|
||||
if (this->outstanding_() != 0) {
|
||||
this->resend_outstanding_();
|
||||
this->start_timer_();
|
||||
}
|
||||
}
|
||||
|
||||
void HdlcReliability::process_ack_(uint8_t nr) {
|
||||
uint8_t acked = (nr - this->va_) & 7;
|
||||
if (acked > this->outstanding_()) {
|
||||
ESP_LOGV(TAG, "%s: Ignoring invalid N(R) %u", this->name_, nr);
|
||||
return;
|
||||
}
|
||||
if (acked == 0)
|
||||
return;
|
||||
// The peer may have had frames that were refused when last resent
|
||||
if (acked > ((this->vn_ - this->va_) & 7))
|
||||
this->vn_ = nr;
|
||||
this->head_ = (this->head_ + acked) % this->max_queue_length_;
|
||||
this->va_ = nr;
|
||||
this->retries_ = 0;
|
||||
if (this->outstanding_() == 0) {
|
||||
this->cancel_timeout(RETRANSMIT_TIMER_ID);
|
||||
} else {
|
||||
this->start_timer_();
|
||||
}
|
||||
}
|
||||
|
||||
void HdlcReliability::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
// Lower layers deliver packets in one piece; the payload is passed up without copying
|
||||
if (!data.is_contiguous()) {
|
||||
ESP_LOGW(TAG, "%s: Packet split across buffers is not supported", this->name_);
|
||||
return;
|
||||
}
|
||||
auto frame = data.as_span();
|
||||
if (frame.size() < FRAME_OVERHEAD) {
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
size_t length = frame.size() - FRAME_OVERHEAD;
|
||||
uint8_t control = frame[0];
|
||||
const uint8_t *payload = frame.data() + 1;
|
||||
uint16_t crc = frame[length + 1] | (frame[length + 2] << 8);
|
||||
if (frame_crc(control, payload, length) != crc) {
|
||||
ESP_LOGV(TAG, "%s: CRC error, frame dropped", this->name_);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
// The peer's packets may only be larger if it has a larger max_packet_size, which the layers above cannot take
|
||||
if (length > this->max_packet_size_) {
|
||||
if (!this->oversize_warned_) {
|
||||
this->oversize_warned_ = true;
|
||||
ESP_LOGW(TAG,
|
||||
"%s: Frame of %zu bytes is larger than %zu, dropped; check that both devices use the same "
|
||||
"max_packet_size",
|
||||
this->name_, length, this->max_packet_size_);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "%s: Frame of %zu bytes is larger than %zu, dropped", this->name_, length, this->max_packet_size_);
|
||||
}
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
this->note_received_(true);
|
||||
|
||||
uint8_t command = control & ~POLL_FINAL_BIT;
|
||||
if (command == UNNUMBERED_SABM) {
|
||||
// A peer that has not heard our UA repeats its SABM, and cannot have sent a numbered frame meanwhile; answering
|
||||
// again keeps this side connected, and the queued frames, instead of treating the repeat as a restart.
|
||||
if (this->state_ == LinkState::LINK_STATE_CONNECTED && this->peer_frame_seen_) {
|
||||
// The peer has restarted, so frames it has not acknowledged are lost
|
||||
ESP_LOGD(TAG, "%s: Link reset by peer", this->name_);
|
||||
this->start_reset_();
|
||||
}
|
||||
this->vr_ = 0;
|
||||
this->rej_sent_ = false;
|
||||
this->send_frame_(UNNUMBERED_UA, nullptr, 0);
|
||||
// Both sides now start from 0, so the link is up even if our own SABM is unanswered
|
||||
this->link_up_();
|
||||
return;
|
||||
}
|
||||
if (command == UNNUMBERED_UA) {
|
||||
if (this->state_ == LinkState::LINK_STATE_RESETTING)
|
||||
this->link_up_();
|
||||
return;
|
||||
}
|
||||
// Numbered frames are ignored until the link is reset; the peer answers our SABM first
|
||||
if (this->state_ != LinkState::LINK_STATE_CONNECTED)
|
||||
return;
|
||||
this->peer_frame_seen_ = true;
|
||||
|
||||
uint8_t nr = control >> 5;
|
||||
if ((control & 1) == 0) {
|
||||
this->process_ack_(nr);
|
||||
this->receive_info_(control, payload, length, meta_data);
|
||||
return;
|
||||
}
|
||||
if ((control & 3) != SUPERVISORY_FRAME)
|
||||
return;
|
||||
this->process_ack_(nr);
|
||||
if (((control >> 2) & 3) == SUPERVISORY_REJ && this->outstanding_() != 0) {
|
||||
ESP_LOGV(TAG, "%s: Peer rejected from %u", this->name_, nr);
|
||||
this->resend_outstanding_();
|
||||
this->start_timer_();
|
||||
}
|
||||
}
|
||||
|
||||
void HdlcReliability::receive_info_(uint8_t control, const uint8_t *payload, size_t length,
|
||||
const PacketMetaData &meta_data) {
|
||||
uint8_t ns = (control >> 1) & 7;
|
||||
if (ns != this->vr_) {
|
||||
// A gap or a duplicate; ask once for a resend from the expected frame, then just acknowledge at once
|
||||
if (!this->rej_sent_) {
|
||||
this->rej_sent_ = this->send_supervisory_(SUPERVISORY_REJ);
|
||||
if (this->rej_sent_)
|
||||
return;
|
||||
}
|
||||
this->owe_ack_(true);
|
||||
return;
|
||||
}
|
||||
this->vr_ = (this->vr_ + 1) & 7;
|
||||
this->rej_sent_ = false;
|
||||
// The acknowledgement is sent from loop(), so a reply sent from the callback can carry it instead
|
||||
this->owe_ack_(false);
|
||||
this->on_receive_from_interface_(PacketBuffer(payload, length), meta_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,99 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_PACKET_INTERFACE_HDLC
|
||||
#include <array>
|
||||
|
||||
#include "packet_interface.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A reliability layer that delivers packets in order, without loss or duplicates, between two peers.
|
||||
*
|
||||
* It follows HDLC asynchronous balanced mode, with sequence numbers modulo 8 and go-back-N retransmission:
|
||||
* - I-frames carry a payload, the sequence number N(S) and an acknowledgement N(R) of the peer's frames.
|
||||
* - RR frames acknowledge without a payload; REJ frames ask the peer to resend from N(R).
|
||||
* - SABM resets both peers' sequence numbers, and UA answers it.
|
||||
*
|
||||
* Each frame is the HDLC control byte, the payload, then a CRC-16 of both in little endian order.
|
||||
* Unacknowledged frames are resent after the timeout; after max_retries the link is reset.
|
||||
*/
|
||||
class HdlcReliability : public PacketInterface {
|
||||
public:
|
||||
/// Most frames that can be unacknowledged at once, set by the 3 bit sequence numbers.
|
||||
static constexpr uint8_t MAX_WINDOW = 7;
|
||||
/// Control byte plus CRC-16.
|
||||
static constexpr size_t FRAME_OVERHEAD = 3;
|
||||
|
||||
explicit HdlcReliability(PacketInterface *lower);
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override;
|
||||
/// No room while the layer has no send queue: before setup(), or when setup() failed.
|
||||
size_t get_send_capacity() const override {
|
||||
return this->tx_slots_ == nullptr ? 0 : this->max_queue_length_ - this->outstanding_();
|
||||
}
|
||||
|
||||
void set_ack_timeout(uint32_t timeout_ms) { this->timeout_ms_ = timeout_ms; }
|
||||
void set_ack_delay(uint32_t ack_delay_ms) { this->ack_delay_ms_ = ack_delay_ms; }
|
||||
void set_max_queue_length(uint8_t length) { this->max_queue_length_ = length; }
|
||||
void set_max_retries(uint8_t retries) { this->max_retries_ = retries; }
|
||||
|
||||
bool is_connected() const { return this->state_ == LinkState::LINK_STATE_CONNECTED; }
|
||||
|
||||
protected:
|
||||
enum class LinkState : uint8_t {
|
||||
LINK_STATE_RESET_PENDING, // SABM to be sent from loop()
|
||||
LINK_STATE_RESETTING, // SABM sent, waiting for UA
|
||||
LINK_STATE_CONNECTED,
|
||||
};
|
||||
|
||||
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
void receive_info_(uint8_t control, const uint8_t *payload, size_t length, const PacketMetaData &meta_data);
|
||||
bool send_frame_(uint8_t control, const uint8_t *payload, size_t length);
|
||||
bool send_supervisory_(uint8_t type);
|
||||
bool send_pending_();
|
||||
void resend_outstanding_();
|
||||
void retry_send_();
|
||||
/// An RR is owed: at once, or after the ack delay unless an I-frame carries it first.
|
||||
void owe_ack_(bool now);
|
||||
void ack_sent_();
|
||||
void process_ack_(uint8_t nr);
|
||||
void on_timeout_();
|
||||
void start_reset_();
|
||||
void link_up_();
|
||||
void start_timer_();
|
||||
void request_loop_();
|
||||
uint8_t outstanding_() const { return (this->vs_ - this->va_) & 7; }
|
||||
uint8_t slot_index_(uint8_t seq) const { return (this->head_ + ((seq - this->va_) & 7)) % this->max_queue_length_; }
|
||||
uint8_t *slot_(uint8_t seq) { return this->tx_slots_ + this->slot_index_(seq) * this->max_packet_size_; }
|
||||
|
||||
PacketInterface *lower_;
|
||||
// max_queue_length_ slots of max_packet_size_ bytes, a ring starting at head_ which holds frame va_; allocated in
|
||||
// setup() and never freed
|
||||
uint8_t *tx_slots_{nullptr};
|
||||
std::array<uint16_t, MAX_WINDOW> tx_lengths_{}; // indexed like the slots
|
||||
uint32_t timeout_ms_{200};
|
||||
uint32_t ack_delay_ms_{0};
|
||||
uint8_t max_queue_length_{4};
|
||||
uint8_t max_retries_{5};
|
||||
uint8_t retries_{0};
|
||||
uint8_t head_{0};
|
||||
uint8_t vs_{0}; // sequence number of the next new frame
|
||||
uint8_t vn_{0}; // next frame to send; frames from va_ up to it have been sent
|
||||
uint8_t va_{0}; // oldest unacknowledged frame
|
||||
uint8_t vr_{0}; // next frame expected from the peer
|
||||
LinkState state_{LinkState::LINK_STATE_RESET_PENDING};
|
||||
bool ack_pending_{false}; // an RR is owed
|
||||
bool ack_due_{false}; // and should be sent now
|
||||
bool rej_sent_{false};
|
||||
bool peer_frame_seen_{false}; // a numbered frame has arrived since the link came up
|
||||
bool oversize_warned_{false};
|
||||
bool reset_wait_logged_{false}; // the queue filled while waiting for this reset to be answered
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,155 +0,0 @@
|
||||
"""The kinds of layer that can be stacked on a packet interface, and the registry of their implementations.
|
||||
|
||||
Each module in this package implements one layer type and registers it with its kind, for example:
|
||||
|
||||
@RELIABILITY.register("hdlc", schema=HDLC_SCHEMA)
|
||||
async def build_hdlc(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
...
|
||||
|
||||
The packet_interface component imports every module here before it builds its schemas, so a new layer type needs
|
||||
only a new module. The kinds, and the order in which they are stacked, are fixed here.
|
||||
"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENCRYPTION, CONF_KEY, CONF_TYPE
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
packet_interface_ns = cg.esphome_ns.namespace("packet_interface")
|
||||
PacketInterface = packet_interface_ns.class_("PacketInterface", cg.Component)
|
||||
|
||||
CONF_RELIABILITY = "reliability"
|
||||
CONF_SESSION = "session"
|
||||
|
||||
# Builds a layer above lower and returns it
|
||||
LayerBuilder = Callable[[ConfigType, MockObj], Awaitable[Any]]
|
||||
|
||||
|
||||
def _never(config: ConfigType) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _nothing(config: ConfigType) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LayerType:
|
||||
"""One implementation of a kind of layer."""
|
||||
|
||||
schema: Any
|
||||
build: LayerBuilder
|
||||
# True if this configuration works only when the peer sends packets back, which a session layer makes sure of
|
||||
needs_session: Callable[[ConfigType], bool]
|
||||
# The components this configuration needs loaded
|
||||
auto_load: Callable[[ConfigType], list[str]]
|
||||
|
||||
|
||||
class LayerKind:
|
||||
"""A kind of layer, such as encryption, with the types that implement it."""
|
||||
|
||||
def __init__(
|
||||
self, key: str, default_type: str, shorthand: str | None = None
|
||||
) -> None:
|
||||
self.key = key
|
||||
self.default_type = default_type
|
||||
# The option that a plain string value sets, as in `encryption: my key`
|
||||
self.shorthand = shorthand
|
||||
self.types: dict[str, LayerType] = {}
|
||||
# Set once the schema is built, as types registered after that would not be in it
|
||||
self.frozen = False
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
schema: Any,
|
||||
needs_session: Callable[[ConfigType], bool] = _never,
|
||||
auto_load: Callable[[ConfigType], list[str]] = _nothing,
|
||||
) -> Callable[[LayerBuilder], LayerBuilder]:
|
||||
"""Register a type with its schema."""
|
||||
|
||||
def decorator(build: LayerBuilder) -> LayerBuilder:
|
||||
if self.frozen:
|
||||
raise ValueError(
|
||||
f"{self.key} layer type '{name}' is registered after the schemas were built; register it "
|
||||
"before the packet_interface component is imported"
|
||||
)
|
||||
if name in self.types:
|
||||
raise ValueError(
|
||||
f"{self.key} layer type '{name}' is already registered"
|
||||
)
|
||||
self.types[name] = LayerType(schema, build, needs_session, auto_load)
|
||||
return build
|
||||
|
||||
return decorator
|
||||
|
||||
def _expand(self, value: Any) -> Any:
|
||||
"""Allow a layer to be enabled with no options, as in `reliability:`, or with its shorthand."""
|
||||
if value is None:
|
||||
return {}
|
||||
if self.shorthand is not None and isinstance(value, str):
|
||||
return {self.shorthand: value}
|
||||
return value
|
||||
|
||||
def schema(self) -> Callable[[Any], ConfigType]:
|
||||
"""The schema for this kind; no type can be registered after this."""
|
||||
self.frozen = True
|
||||
return cv.All(
|
||||
self._expand,
|
||||
cv.typed_schema(
|
||||
{name: layer.schema for name, layer in self.types.items()},
|
||||
default_type=self.default_type,
|
||||
),
|
||||
)
|
||||
|
||||
def get_type(self, config: ConfigType) -> LayerType:
|
||||
return self.types[config[CONF_TYPE]]
|
||||
|
||||
|
||||
ENCRYPTION = LayerKind(CONF_ENCRYPTION, "xxtea", shorthand=CONF_KEY)
|
||||
RELIABILITY = LayerKind(CONF_RELIABILITY, "hdlc")
|
||||
SESSION = LayerKind(CONF_SESSION, "basic")
|
||||
# The kinds stacked above the link, from the bottom up
|
||||
STACK_ORDER = (ENCRYPTION, RELIABILITY, SESSION)
|
||||
|
||||
|
||||
def stack_schema() -> dict:
|
||||
"""Schema entries for the layers of one stack; call only once all layer types are registered."""
|
||||
return {cv.Optional(kind.key): kind.schema() for kind in STACK_ORDER}
|
||||
|
||||
|
||||
def check_session(config: ConfigType) -> None:
|
||||
"""Layers that need packets back from the peer need a session layer, whose keep-alives send them."""
|
||||
if CONF_SESSION in config:
|
||||
return
|
||||
for kind in STACK_ORDER:
|
||||
if (layer := config.get(kind.key)) is not None and kind.get_type(
|
||||
layer
|
||||
).needs_session(layer):
|
||||
raise cv.Invalid(
|
||||
f"This '{kind.key}' needs packets from the peer, so it needs a '{CONF_SESSION}' layer, whose "
|
||||
"keep-alives make sure they are sent",
|
||||
path=[kind.key],
|
||||
)
|
||||
|
||||
|
||||
def stack_auto_load(config: ConfigType) -> set[str]:
|
||||
"""The components that the layers configured in config need loaded."""
|
||||
loads: set[str] = set()
|
||||
for kind in STACK_ORDER:
|
||||
if (layer := config.get(kind.key)) is not None:
|
||||
loads.update(kind.get_type(layer).auto_load(layer))
|
||||
return loads
|
||||
|
||||
|
||||
async def build_stack(config: ConfigType, lower: MockObj) -> None:
|
||||
"""Create the layers configured in config, from the bottom up, above lower."""
|
||||
for kind in STACK_ORDER:
|
||||
if (layer := config.get(kind.key)) is not None:
|
||||
lower = await kind.get_type(layer).build(layer, lower)
|
||||
@@ -1,70 +0,0 @@
|
||||
"""Session layer that sends keep-alives and reports whether the peer can be reached."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BINARY_SENSOR,
|
||||
CONF_ID,
|
||||
CONF_TIMEOUT,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import SESSION, PacketInterface, packet_interface_ns
|
||||
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
|
||||
BasicSession = packet_interface_ns.class_("BasicSession", PacketInterface)
|
||||
|
||||
|
||||
def _session_timeout(config: ConfigType) -> ConfigType:
|
||||
"""Default the timeout to three keep-alive intervals, and require it to be longer than one."""
|
||||
keep_alive = config[CONF_KEEP_ALIVE].total_milliseconds
|
||||
if (timeout := config.get(CONF_TIMEOUT)) is None:
|
||||
config[CONF_TIMEOUT] = cv.positive_time_period_milliseconds(
|
||||
f"{keep_alive * 3}ms"
|
||||
)
|
||||
elif timeout.total_milliseconds <= keep_alive:
|
||||
raise cv.Invalid("Must be longer than keep_alive", path=[CONF_TIMEOUT])
|
||||
return config
|
||||
|
||||
|
||||
BASIC_SESSION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BasicSession),
|
||||
cv.Optional(CONF_KEEP_ALIVE, default="10s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(hours=1)),
|
||||
),
|
||||
cv.Optional(CONF_TIMEOUT): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(hours=3)),
|
||||
),
|
||||
cv.Optional(CONF_BINARY_SENSOR): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_session_timeout,
|
||||
)
|
||||
|
||||
|
||||
@SESSION.register(
|
||||
"basic",
|
||||
schema=BASIC_SESSION_SCHEMA,
|
||||
auto_load=lambda config: ["binary_sensor"] if CONF_BINARY_SENSOR in config else [],
|
||||
)
|
||||
async def build_basic_session(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
cg.add_define("USE_PACKET_INTERFACE_BASIC_SESSION")
|
||||
var = cg.new_Pvariable(config[CONF_ID], lower)
|
||||
cg.add(var.set_keep_alive(config[CONF_KEEP_ALIVE]))
|
||||
cg.add(var.set_session_timeout(config[CONF_TIMEOUT]))
|
||||
await cg.register_component(var, config)
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_BINARY_SENSOR, var.set_connected_sensor)
|
||||
return var
|
||||
@@ -1,59 +0,0 @@
|
||||
"""Reliability layer using HDLC style acknowledgement and go-back-N retransmission."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TIMEOUT
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import RELIABILITY, PacketInterface, packet_interface_ns
|
||||
|
||||
CONF_ACK_DELAY = "ack_delay"
|
||||
CONF_MAX_QUEUE_LENGTH = "max_queue_length"
|
||||
CONF_MAX_RETRIES = "max_retries"
|
||||
|
||||
HdlcReliability = packet_interface_ns.class_("HdlcReliability", PacketInterface)
|
||||
|
||||
|
||||
def _ack_delay_within_timeout(config: ConfigType) -> ConfigType:
|
||||
"""The peer, configured the same, must hear the acknowledgement before its timeout."""
|
||||
if (
|
||||
config[CONF_ACK_DELAY].total_milliseconds
|
||||
>= config[CONF_TIMEOUT].total_milliseconds
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Must be shorter than '{CONF_TIMEOUT}'", path=[CONF_ACK_DELAY]
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
HDLC_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HdlcReliability),
|
||||
# Retransmitting more often than the main loop runs would exhaust the retries at once
|
||||
cv.Optional(CONF_TIMEOUT, default="200ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(milliseconds=50)),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ACK_DELAY, default="0ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_MAX_QUEUE_LENGTH, default=4): cv.int_range(1, 7),
|
||||
cv.Optional(CONF_MAX_RETRIES, default=5): cv.int_range(1, 255),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_ack_delay_within_timeout,
|
||||
)
|
||||
|
||||
|
||||
@RELIABILITY.register("hdlc", schema=HDLC_SCHEMA)
|
||||
async def build_hdlc(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
cg.add_define("USE_PACKET_INTERFACE_HDLC")
|
||||
var = cg.new_Pvariable(config[CONF_ID], lower)
|
||||
cg.add(var.set_ack_timeout(config[CONF_TIMEOUT]))
|
||||
cg.add(var.set_ack_delay(config[CONF_ACK_DELAY]))
|
||||
cg.add(var.set_max_queue_length(config[CONF_MAX_QUEUE_LENGTH]))
|
||||
cg.add(var.set_max_retries(config[CONF_MAX_RETRIES]))
|
||||
await cg.register_component(var, config)
|
||||
return var
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Encryption layer using XXTEA, with an optional rolling code."""
|
||||
|
||||
import hashlib
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_KEY
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ENCRYPTION, PacketInterface, packet_interface_ns
|
||||
|
||||
CONF_ROLLING_CODE_ENABLE = "rolling_code_enable"
|
||||
|
||||
# A captured packet lets a guessed key be checked offline, so the key text is stretched to make each guess costly.
|
||||
# Both ends derive the same key from the same text, so the salt is fixed; it keeps tables built for other uses of
|
||||
# PBKDF2 from applying here.
|
||||
KEY_SALT = b"esphome packet_interface xxtea"
|
||||
KEY_ITERATIONS = 600_000
|
||||
|
||||
XxteaEncryption = packet_interface_ns.class_("XxteaEncryption", PacketInterface)
|
||||
|
||||
XXTEA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(XxteaEncryption),
|
||||
cv.Required(CONF_KEY): cv.All(cv.sensitive(cv.string), cv.Length(min=1)),
|
||||
cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def derive_key(text: str) -> list[int]:
|
||||
"""The 256-bit key for the key text, as 32-bit words in little endian order."""
|
||||
digest = hashlib.pbkdf2_hmac("sha256", text.encode(), KEY_SALT, KEY_ITERATIONS)
|
||||
return [int.from_bytes(digest[i : i + 4], "little") for i in range(0, 32, 4)]
|
||||
|
||||
|
||||
@ENCRYPTION.register(
|
||||
"xxtea",
|
||||
schema=XXTEA_SCHEMA,
|
||||
# The rolling code accepts a packet only once the peer has sent back this device's boot number
|
||||
needs_session=lambda config: config[CONF_ROLLING_CODE_ENABLE],
|
||||
auto_load=lambda config: ["xxtea"],
|
||||
)
|
||||
async def build_xxtea(config: ConfigType, lower: MockObj) -> MockObj:
|
||||
cg.add_define("USE_PACKET_INTERFACE_XXTEA")
|
||||
var = cg.new_Pvariable(config[CONF_ID], lower, config[CONF_ROLLING_CODE_ENABLE])
|
||||
cg.add(var.set_key(derive_key(config[CONF_KEY])))
|
||||
await cg.register_component(var, config)
|
||||
return var
|
||||
@@ -1,182 +0,0 @@
|
||||
#include "xxtea_encryption.h"
|
||||
#ifdef USE_PACKET_INTERFACE_XXTEA
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "esphome/components/xxtea/xxtea.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.xxtea");
|
||||
|
||||
static constexpr uint32_t PREF_HASH = fnv1a_hash("packet_interface.xxtea");
|
||||
// Different for each mode, so devices set up differently drop each other's packets
|
||||
static constexpr uint8_t CHECK_BYTES[3] = {0x58, 0x54, 0x45};
|
||||
static constexpr uint8_t ROLLING_CHECK_BYTES[3] = {0x58, 0x54, 0x52};
|
||||
static constexpr size_t CHECK_SIZE = 4;
|
||||
// Boot number, counter, and the echo of the receiver's boot number
|
||||
static constexpr size_t ROLLING_SIZE = 12;
|
||||
// XXTEA needs at least two words
|
||||
static constexpr size_t MIN_LENGTH = 8;
|
||||
static constexpr size_t MAX_PADDING = 4;
|
||||
|
||||
static void put_uint32(uint8_t *buf, uint32_t value) {
|
||||
buf[0] = value;
|
||||
buf[1] = value >> 8;
|
||||
buf[2] = value >> 16;
|
||||
buf[3] = value >> 24;
|
||||
}
|
||||
|
||||
static uint32_t get_uint32(const uint8_t *buf) { return encode_uint32(buf[3], buf[2], buf[1], buf[0]); }
|
||||
|
||||
static constexpr size_t trailer_size(bool rolling_code) {
|
||||
return rolling_code ? ROLLING_SIZE + CHECK_SIZE : CHECK_SIZE;
|
||||
}
|
||||
|
||||
XxteaEncryption::XxteaEncryption(PacketInterface *lower, bool rolling_code)
|
||||
// XXTEA works on whole 32-bit words
|
||||
: PacketInterface(remaining_packet_size(lower->get_max_packet_size() & ~size_t{3}, trailer_size(rolling_code))),
|
||||
lower_(lower),
|
||||
trailer_size_(trailer_size(rolling_code)),
|
||||
rolling_code_(rolling_code) {
|
||||
this->stack_on_(lower, CAPABILITY_ENCRYPTION);
|
||||
}
|
||||
|
||||
void XxteaEncryption::set_key(std::initializer_list<uint32_t> key) {
|
||||
std::copy_n(key.begin(), std::min(key.size(), this->key_.size()), this->key_.begin());
|
||||
}
|
||||
|
||||
void XxteaEncryption::setup() {
|
||||
if (this->max_packet_size_ == 0) {
|
||||
this->mark_failed(LOG_STR("max_packet_size too small for the configured layers"));
|
||||
return;
|
||||
}
|
||||
size_t words = this->lower_->get_max_packet_size() / 4;
|
||||
RAMAllocator<uint32_t> allocator;
|
||||
this->tx_buffer_ = allocator.allocate(words * 2);
|
||||
if (this->tx_buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Not enough memory for the packet buffers"));
|
||||
return;
|
||||
}
|
||||
this->rx_buffer_ = this->tx_buffer_ + words;
|
||||
if (this->rolling_code_) {
|
||||
this->pref_ = global_preferences->make_preference<uint32_t>(fnv1a_hash_extend(PREF_HASH, this->name_), true);
|
||||
this->pref_.load(&this->boot_);
|
||||
this->next_boot_();
|
||||
}
|
||||
this->lower_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &meta_data) { this->receive_(data, meta_data); });
|
||||
}
|
||||
|
||||
void XxteaEncryption::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"XXTEA Encryption on '%s':\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Rolling Code: %s",
|
||||
this->name_, this->max_packet_size_, YESNO(this->rolling_code_));
|
||||
if (this->rolling_code_) {
|
||||
ESP_LOGCONFIG(TAG, " Boot Number: %" PRIu32, this->boot_);
|
||||
}
|
||||
}
|
||||
|
||||
// A new boot number must be saved before it is used, or a crash could make the peer see it again
|
||||
void XxteaEncryption::next_boot_() {
|
||||
this->boot_++;
|
||||
this->counter_ = 0;
|
||||
if (!this->pref_.save(&this->boot_) || !global_preferences->sync()) {
|
||||
ESP_LOGW(TAG, "%s: Could not save the boot number, so a restart could reuse it", this->name_);
|
||||
}
|
||||
}
|
||||
|
||||
bool XxteaEncryption::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
if (this->tx_buffer_ == nullptr)
|
||||
return false;
|
||||
size_t size = data.size();
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
size_t padding = (4 - (size + this->trailer_size_) % 4) % 4;
|
||||
if (size + padding + this->trailer_size_ < MIN_LENGTH)
|
||||
padding += 4;
|
||||
size_t length = size + padding + this->trailer_size_;
|
||||
auto *buf = reinterpret_cast<uint8_t *>(this->tx_buffer_);
|
||||
data.copy_to(buf, size);
|
||||
memset(buf + size, 0, padding);
|
||||
uint8_t *check = buf + length - CHECK_SIZE;
|
||||
if (this->rolling_code_) {
|
||||
put_uint32(check - ROLLING_SIZE, this->boot_);
|
||||
put_uint32(check - ROLLING_SIZE + 4, this->counter_);
|
||||
put_uint32(check - ROLLING_SIZE + 8, this->echo_boot_);
|
||||
memcpy(check, ROLLING_CHECK_BYTES, sizeof(ROLLING_CHECK_BYTES));
|
||||
if (++this->counter_ == 0)
|
||||
this->next_boot_();
|
||||
} else {
|
||||
memcpy(check, CHECK_BYTES, sizeof(CHECK_BYTES));
|
||||
}
|
||||
check[CHECK_SIZE - 1] = padding;
|
||||
xxtea::encrypt(this->tx_buffer_, length / 4, this->key_.data());
|
||||
return this->lower_->send_to_interface(PacketBuffer(buf, length), meta_data);
|
||||
}
|
||||
|
||||
bool XxteaEncryption::accept_counter_(uint32_t boot, uint32_t counter) {
|
||||
if (this->peer_seen_ && (boot < this->peer_boot_ || (boot == this->peer_boot_ && counter <= this->peer_counter_)))
|
||||
return false;
|
||||
this->peer_seen_ = true;
|
||||
this->peer_boot_ = boot;
|
||||
this->peer_counter_ = counter;
|
||||
return true;
|
||||
}
|
||||
|
||||
void XxteaEncryption::receive_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
size_t length = data.size();
|
||||
if (length % 4 != 0 || length < std::max(MIN_LENGTH, this->trailer_size_) ||
|
||||
length > this->lower_->get_max_packet_size()) {
|
||||
ESP_LOGV(TAG, "%s: Packet of %zu bytes dropped", this->name_, length);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
auto *buf = reinterpret_cast<uint8_t *>(this->rx_buffer_);
|
||||
data.copy_to(buf, length);
|
||||
xxtea::decrypt(this->rx_buffer_, length / 4, this->key_.data());
|
||||
const uint8_t *check = buf + length - CHECK_SIZE;
|
||||
const uint8_t *expected = this->rolling_code_ ? ROLLING_CHECK_BYTES : CHECK_BYTES;
|
||||
size_t padding = check[CHECK_SIZE - 1];
|
||||
if (memcmp(check, expected, sizeof(CHECK_BYTES)) != 0 || padding > MAX_PADDING ||
|
||||
padding > length - this->trailer_size_) {
|
||||
ESP_LOGV(TAG, "%s: Packet failed authentication", this->name_);
|
||||
this->note_received_(false);
|
||||
return;
|
||||
}
|
||||
this->note_received_(true);
|
||||
if (this->rolling_code_) {
|
||||
uint32_t boot = get_uint32(check - ROLLING_SIZE);
|
||||
uint32_t counter = get_uint32(check - ROLLING_SIZE + 4);
|
||||
uint32_t echo = get_uint32(check - ROLLING_SIZE + 8);
|
||||
// Learn the peer's boot number even from a packet that is then dropped, so both sides can catch up
|
||||
if (boot > this->echo_boot_)
|
||||
this->echo_boot_ = boot;
|
||||
// A packet recorded before this boot cannot carry this boot's number
|
||||
if (echo != this->boot_) {
|
||||
ESP_LOGV(TAG, "%s: Packet for boot %" PRIu32 " dropped", this->name_, echo);
|
||||
return;
|
||||
}
|
||||
if (!this->accept_counter_(boot, counter)) {
|
||||
// A boot number lower than one already heard means the peer lost its saved boot number, or a replay
|
||||
if (boot < this->peer_boot_ && !this->old_boot_warned_) {
|
||||
this->old_boot_warned_ = true;
|
||||
ESP_LOGW(TAG,
|
||||
"%s: Packets from an older boot %" PRIu32 " of the peer are dropped; if its flash was erased, "
|
||||
"restart this device",
|
||||
this->name_, boot);
|
||||
}
|
||||
ESP_LOGV(TAG, "%s: Old packet %" PRIu32 ":%" PRIu32 " dropped", this->name_, boot, counter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->on_receive_from_interface_(PacketBuffer(buf, length - this->trailer_size_ - padding), meta_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_PACKET_INTERFACE_XXTEA
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "packet_interface.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* An encryption layer that encrypts and authenticates each packet with XXTEA and a 256-bit shared key.
|
||||
*
|
||||
* The packet is padded with zeros and followed by a trailer, then the whole is encrypted as one block. With the
|
||||
* rolling code enabled, the trailer starts with:
|
||||
* - a 32-bit boot number, saved in flash and increased at every boot;
|
||||
* - a 32-bit packet counter, starting at 0 at every boot;
|
||||
* - the highest boot number heard from the peer, echoed back to it.
|
||||
* The trailer always ends with 3 fixed check bytes and the number of padding bytes. XXTEA mixes the whole block, so
|
||||
* a packet that was changed or encrypted with another key fails the check.
|
||||
* With the rolling code, a packet is dropped as a replay unless it echoes this device's current boot number, and its
|
||||
* boot number and counter are higher than those of the last packet accepted from the peer. A boot number is never
|
||||
* reused, so the echo acts as a fresh challenge after every restart.
|
||||
*/
|
||||
class XxteaEncryption : public PacketInterface {
|
||||
public:
|
||||
XxteaEncryption(PacketInterface *lower, bool rolling_code);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) override;
|
||||
|
||||
void set_key(std::initializer_list<uint32_t> key);
|
||||
|
||||
protected:
|
||||
void receive_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
bool accept_counter_(uint32_t boot, uint32_t counter);
|
||||
void next_boot_();
|
||||
|
||||
PacketInterface *lower_;
|
||||
const size_t trailer_size_;
|
||||
std::array<uint32_t, 8> key_{};
|
||||
// Separate buffers, as a layer above may send a reply while a received packet is being passed up; both are in one
|
||||
// block allocated in setup(), and tx_buffer_ stays null if that fails
|
||||
uint32_t *tx_buffer_{nullptr};
|
||||
uint32_t *rx_buffer_{nullptr};
|
||||
ESPPreferenceObject pref_;
|
||||
uint32_t boot_{0};
|
||||
uint32_t counter_{0};
|
||||
uint32_t peer_boot_{0};
|
||||
uint32_t peer_counter_{0};
|
||||
uint32_t echo_boot_{0}; // highest boot number heard from the peer
|
||||
bool peer_seen_{false};
|
||||
bool old_boot_warned_{false};
|
||||
const bool rolling_code_;
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
#endif
|
||||
@@ -30,10 +30,6 @@ void PCF8563Component::read_time() {
|
||||
ESP_LOGW(TAG, "RTC halted, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
if (this->pcf8563_.reg.voltage_low) {
|
||||
ESP_LOGW(TAG, "Voltage-low flag set, RTC time may be invalid; not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
ESPTime rtc_time{
|
||||
.second = uint8_t(pcf8563_.reg.second + 10 * pcf8563_.reg.second_10),
|
||||
.minute = uint8_t(pcf8563_.reg.minute + 10u * pcf8563_.reg.minute_10),
|
||||
@@ -71,7 +67,6 @@ void PCF8563Component::write_time() {
|
||||
pcf8563_.reg.second = now.second % 10;
|
||||
pcf8563_.reg.second_10 = now.second / 10;
|
||||
pcf8563_.reg.stop = false;
|
||||
this->pcf8563_.reg.voltage_low = false;
|
||||
|
||||
this->write_rtc_();
|
||||
}
|
||||
@@ -81,10 +76,10 @@ bool PCF8563Component::read_rtc_() {
|
||||
ESP_LOGE(TAG, "Can't read I2C data.");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u STOP:%s CLKOUT:%0u VL:%0u", pcf8563_.reg.hour_10,
|
||||
ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u STOP:%s CLKOUT:%0u", pcf8563_.reg.hour_10,
|
||||
pcf8563_.reg.hour, pcf8563_.reg.minute_10, pcf8563_.reg.minute, pcf8563_.reg.second_10, pcf8563_.reg.second,
|
||||
pcf8563_.reg.year_10, pcf8563_.reg.year, pcf8563_.reg.month_10, pcf8563_.reg.month, pcf8563_.reg.day_10,
|
||||
pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled, pcf8563_.reg.voltage_low);
|
||||
pcf8563_.reg.day, ONOFF(!pcf8563_.reg.stop), pcf8563_.reg.clkout_enabled);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ class PCF8563Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
// Seconds register
|
||||
uint8_t second : 4;
|
||||
uint8_t second_10 : 3;
|
||||
bool voltage_low : 1;
|
||||
bool clock_int : 1;
|
||||
|
||||
// Minutes register
|
||||
uint8_t minute : 4;
|
||||
|
||||
@@ -1277,16 +1277,12 @@ register_protocol_action(
|
||||
PRONTO_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_DATA): cv.string,
|
||||
}
|
||||
)
|
||||
PRONTO_BINARY_SENSOR_SCHEMA = PRONTO_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_DELTA, default=-1): cv.int_,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_binary_sensor("pronto", ProntoBinarySensor, PRONTO_BINARY_SENSOR_SCHEMA)
|
||||
@register_binary_sensor("pronto", ProntoBinarySensor, PRONTO_SCHEMA)
|
||||
def pronto_binary_sensor(var, config):
|
||||
cg.add(
|
||||
var.set_data(
|
||||
@@ -1309,6 +1305,7 @@ def pronto_dumper(var, config):
|
||||
pass
|
||||
|
||||
|
||||
# delta only matters when matching a received code
|
||||
register_protocol_action(
|
||||
"pronto", "Pronto", PRONTO_SCHEMA, (CONF_DATA, "data", cg.std_string)
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -13,9 +12,6 @@ namespace esphome::runtime_image {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image_decoder.bmp");
|
||||
|
||||
// BMP has no alpha. Every pixel is drawn opaque, or a transparent image would hide it
|
||||
static constexpr Color OPAQUE_BLACK(0, 0, 0, 0xFF);
|
||||
|
||||
void BmpDecoder::reset() {
|
||||
ImageDecoder::reset();
|
||||
this->bits_per_pixel_ = 0;
|
||||
@@ -146,31 +142,23 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
this->current_index_ = this->data_offset_;
|
||||
index = this->data_offset_;
|
||||
}
|
||||
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
uint32_t steps = 0;
|
||||
auto out_of_time = [start, &steps]() {
|
||||
return ++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS;
|
||||
};
|
||||
// Bytes after the last pixel are used up without drawing anything
|
||||
const size_t total_pixels = static_cast<size_t>(this->width_) * this->height_;
|
||||
switch (this->bits_per_pixel_) {
|
||||
case 1: {
|
||||
size_t width = static_cast<size_t>(this->width_);
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
while (index < size) {
|
||||
size_t x = this->paint_index_ % width;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
|
||||
size_t remaining_in_row = width - x;
|
||||
uint8_t pixels_in_byte = std::min<size_t>(remaining_in_row, 8);
|
||||
bool end_of_row = remaining_in_row <= 8;
|
||||
size_t needed = 1 + (end_of_row ? this->padding_bytes_ : 0);
|
||||
if (index + needed > size || out_of_time()) {
|
||||
if (index + needed > size) {
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
}
|
||||
uint8_t current_byte = buffer[index];
|
||||
for (uint8_t i = 0; i < pixels_in_byte; i++) {
|
||||
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : OPAQUE_BLACK;
|
||||
Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF;
|
||||
this->draw(x + i, y, 1, 1, c);
|
||||
}
|
||||
this->paint_index_ += pixels_in_byte;
|
||||
@@ -187,11 +175,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
case 8: {
|
||||
size_t width = static_cast<size_t>(this->width_);
|
||||
size_t last_col = width - 1;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
while (index < size) {
|
||||
size_t x = this->paint_index_ % width;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
|
||||
size_t needed = 1 + ((x == last_col) ? this->padding_bytes_ : 0);
|
||||
if (index + needed > size || out_of_time()) {
|
||||
if (index + needed > size) {
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
}
|
||||
@@ -206,7 +194,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
uint8_t b = rgb & 0xff;
|
||||
uint8_t g = (rgb >> 8) & 0xff;
|
||||
uint8_t r = (rgb >> 16) & 0xff;
|
||||
this->draw(x, y, 1, 1, Color(r, g, b, 0xFF));
|
||||
this->draw(x, y, 1, 1, Color(r, g, b));
|
||||
this->paint_index_++;
|
||||
this->current_index_++;
|
||||
index++;
|
||||
@@ -220,18 +208,18 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
case 24: {
|
||||
size_t width = static_cast<size_t>(this->width_);
|
||||
size_t last_col = width - 1;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
while (index < size) {
|
||||
size_t x = this->paint_index_ % width;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
|
||||
size_t needed = 3 + ((x == last_col) ? this->padding_bytes_ : 0);
|
||||
if (index + needed > size || out_of_time()) {
|
||||
if (index + needed > size) {
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
}
|
||||
uint8_t b = buffer[index];
|
||||
uint8_t g = buffer[index + 1];
|
||||
uint8_t r = buffer[index + 2];
|
||||
this->draw(x, y, 1, 1, Color(r, g, b, 0xFF));
|
||||
this->draw(x, y, 1, 1, Color(r, g, b));
|
||||
this->paint_index_++;
|
||||
this->current_index_ += 3;
|
||||
index += 3;
|
||||
|
||||
@@ -1,106 +1,29 @@
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image_decoder");
|
||||
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
if (!this->resize_image_(width, height))
|
||||
return false;
|
||||
this->prepare_draw_(width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImageDecoder::resize_image_(int width, int height) {
|
||||
this->size_valid_ = this->image_->resize(width, height) > 0;
|
||||
return this->size_valid_;
|
||||
}
|
||||
|
||||
void ImageDecoder::prepare_draw_(int width, int height) {
|
||||
this->draw_width_ = width;
|
||||
this->draw_height_ = height;
|
||||
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
|
||||
if (!this->direct_rgb565_)
|
||||
return;
|
||||
if ((this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) ||
|
||||
(this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1))) {
|
||||
ESP_LOGW(TAG, "No memory to draw straight into the buffer; drawing pixel by pixel");
|
||||
this->direct_rgb565_ = false;
|
||||
// Free the table that did fit, as nothing uses it
|
||||
this->x_boundaries_.release();
|
||||
this->y_boundaries_.release();
|
||||
return;
|
||||
}
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
// The same boundaries as draw() uses, so both paths paint the same pixels
|
||||
for (int x = 0; x <= width; x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(x, width, buffer_width)));
|
||||
}
|
||||
for (int y = 0; y <= height; y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(y, height, buffer_height)));
|
||||
}
|
||||
}
|
||||
|
||||
void ImageDecoder::draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565) {
|
||||
const int x_end = std::min(x + w, this->draw_width_);
|
||||
const int y_end = std::min(y + h, this->draw_height_);
|
||||
x = std::max(x, 0);
|
||||
y = std::max(y, 0);
|
||||
if (x >= x_end || y >= y_end)
|
||||
return;
|
||||
// A block of source pixels lands on one block of the buffer
|
||||
const uint16_t x1 = this->x_boundaries_[x];
|
||||
const uint16_t x2 = this->x_boundaries_[x_end];
|
||||
const uint16_t y1 = this->y_boundaries_[y];
|
||||
const uint16_t y2 = this->y_boundaries_[y_end];
|
||||
const bool big_endian = this->image_is_big_endian_();
|
||||
const uint8_t first = big_endian ? rgb565 >> 8 : rgb565 & 0xFF;
|
||||
const uint8_t second = big_endian ? rgb565 & 0xFF : rgb565 >> 8;
|
||||
uint8_t *const buffer = this->image_buffer_();
|
||||
const uint32_t buffer_width = this->image_->get_buffer_width();
|
||||
for (uint32_t row = y1; row < y2; row++) {
|
||||
uint8_t *destination = buffer + (row * buffer_width + x1) * 2;
|
||||
for (uint32_t col = x1; col < x2; col++) {
|
||||
destination[0] = first;
|
||||
destination[1] = second;
|
||||
destination += 2;
|
||||
}
|
||||
}
|
||||
bool success = this->image_->resize(width, height) > 0;
|
||||
this->size_valid_ = success;
|
||||
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||
return success;
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
if (!this->size_valid_) {
|
||||
return;
|
||||
}
|
||||
if (this->direct_rgb565_) {
|
||||
this->draw_rgb565_(x, y, w, h, display::ColorUtil::color_to_565(color));
|
||||
return;
|
||||
}
|
||||
// When shrinking, each target pixel is painted once, by the first source pixel that reaches it, rather than by every
|
||||
// pixel that lands on it. Integer maths: the S3 has no double FPU.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
int x1 = x;
|
||||
int y1 = y;
|
||||
int x2 = x + w;
|
||||
int y2 = y + h;
|
||||
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
|
||||
x1 = scale_boundary(x, this->draw_width_, buffer_width);
|
||||
y1 = scale_boundary(y, this->draw_height_, buffer_height);
|
||||
x2 = scale_boundary(x + w, this->draw_width_, buffer_width);
|
||||
y2 = scale_boundary(y + h, this->draw_height_, buffer_height);
|
||||
}
|
||||
x2 = std::min(buffer_width, x2);
|
||||
y2 = std::min(buffer_height, y2);
|
||||
for (int i = x1; i < x2; i++) {
|
||||
for (int j = y1; j < y2; j++) {
|
||||
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
this->image_->draw_pixel(i, j, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "image_format.h"
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/// The first of `target` pixels that pixel n of `source` paints, rounded up: ceil(n * target / source). The spans of
|
||||
/// neighbouring pixels meet without overlapping, so a shrunk image paints each target pixel once.
|
||||
inline int scale_boundary(int n, int source, int target) {
|
||||
if (source <= 0)
|
||||
return 0;
|
||||
return static_cast<int>((static_cast<uint32_t>(n) * static_cast<uint32_t>(target) + source - 1) /
|
||||
static_cast<uint32_t>(source));
|
||||
}
|
||||
|
||||
enum DecodeError : int {
|
||||
DECODE_ERROR_INVALID_TYPE = -1,
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
|
||||
@@ -48,11 +38,6 @@ constexpr const char *decode_error_to_string(int error) {
|
||||
}
|
||||
}
|
||||
|
||||
// The longest one decode() call works before it returns, leaving the rest of the data for the next call
|
||||
static constexpr uint32_t DECODE_TIME_LIMIT_MS = 15;
|
||||
// Pixel loops read the clock once per this many steps
|
||||
static constexpr uint32_t DECODE_TIME_CHECK_STEPS = 64;
|
||||
|
||||
class RuntimeImage;
|
||||
|
||||
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
|
||||
@@ -98,9 +83,8 @@ class ImageDecoder {
|
||||
this->expected_size_ = 0;
|
||||
this->decoded_bytes_ = 0;
|
||||
this->size_valid_ = true;
|
||||
this->draw_width_ = 0;
|
||||
this->draw_height_ = 0;
|
||||
this->direct_rgb565_ = false;
|
||||
this->x_scale_ = 1.0;
|
||||
this->y_scale_ = 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,37 +179,19 @@ class ImageDecoder {
|
||||
/// Whether is_finished() can tell a complete image from a cut-off one
|
||||
virtual bool knows_end() const { return this->expected_size_ > 0; }
|
||||
|
||||
/// The image is opaque RGB565, so draw() writes it straight into the buffer
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
|
||||
protected:
|
||||
/// Resize the image for a width x height source, without preparing to draw. prepare_draw_() must follow.
|
||||
bool resize_image_(int width, int height);
|
||||
/**
|
||||
* Draw a source of width x height over the whole buffer from now on. For an opaque RGB565 image this builds the
|
||||
* tables of where each source column and row lands; without the memory for them it draws pixel by pixel instead.
|
||||
*/
|
||||
void prepare_draw_(int width, int height);
|
||||
/// Paint a block of source pixels in one color straight into the buffer. Only while uses_direct_rgb565().
|
||||
void draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565);
|
||||
|
||||
/// The image's pixel buffer, for decoders that write their output straight into it
|
||||
uint8_t *image_buffer_() const;
|
||||
bool image_is_big_endian_() const;
|
||||
|
||||
// Size of the image as the decoder draws it, which draw() spreads over the buffer
|
||||
int draw_width_ = 0;
|
||||
int draw_height_ = 0;
|
||||
// Boundary n is the first buffer column, or row, that source column, or row, n paints
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
RuntimeImage *image_;
|
||||
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||
const ImageFormat format_;
|
||||
bool active_ = false; // A decoding session is in progress
|
||||
bool size_valid_ = true; // Last set_size() result; draw() no-ops while false
|
||||
bool direct_rgb565_ = false;
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
@@ -16,6 +16,32 @@ ESPHOME_LOG_TAG(TAG, "image_decoder.jpeg");
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
|
||||
if (this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) {
|
||||
return false;
|
||||
}
|
||||
if (this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->source_width_ = width;
|
||||
this->source_height_ = height;
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const uint32_t target_width = this->image_->get_buffer_width();
|
||||
const uint32_t target_height = this->image_->get_buffer_height();
|
||||
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
|
||||
// the generic draw() path rounds down and keeps the odd ones instead
|
||||
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
|
||||
}
|
||||
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels) {
|
||||
if (pixels == nullptr) {
|
||||
return;
|
||||
@@ -26,14 +52,14 @@ void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels)
|
||||
const uint8_t first = this->image_is_big_endian_() ? 1 : 0;
|
||||
for (int source_y = 0; source_y < h; source_y++) {
|
||||
const int absolute_y = y + source_y;
|
||||
if (absolute_y < 0 || absolute_y >= this->draw_height_) {
|
||||
if (absolute_y < 0 || absolute_y >= this->source_height_) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t y1 = this->y_boundaries_[absolute_y];
|
||||
const uint16_t y2 = this->y_boundaries_[absolute_y + 1];
|
||||
for (int source_x = 0; source_x < w; source_x++) {
|
||||
const int absolute_x = x + source_x;
|
||||
if (absolute_x < 0 || absolute_x >= this->draw_width_) {
|
||||
if (absolute_x < 0 || absolute_x >= this->source_width_) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t x1 = this->x_boundaries_[absolute_x];
|
||||
@@ -131,10 +157,17 @@ int JpegDecoder::start_() {
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
|
||||
|
||||
this->jpeg_.setUserPointer(this);
|
||||
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
|
||||
if (this->direct_rgb565_) {
|
||||
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
|
||||
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
|
||||
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
}
|
||||
const int width = this->jpeg_.getWidth();
|
||||
const int height = this->jpeg_.getHeight();
|
||||
// The tables for drawing are only built once the decoded size is known, below
|
||||
if (!this->resize_image_(width, height)) {
|
||||
if (!this->set_size(width, height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
|
||||
@@ -148,16 +181,13 @@ int JpegDecoder::start_() {
|
||||
}
|
||||
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
|
||||
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
|
||||
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
|
||||
this->y_scale_ = static_cast<double>(buffer_height) / decoded_height;
|
||||
if (this->scale_ != 1) {
|
||||
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
|
||||
}
|
||||
this->prepare_draw_(decoded_width, decoded_height);
|
||||
if (this->direct_rgb565_) {
|
||||
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
|
||||
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
|
||||
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ class JpegDecoder : public ImageDecoder {
|
||||
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
|
||||
int HOT decode_step() override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
|
||||
int get_scale() const { return this->scale_; }
|
||||
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
|
||||
|
||||
protected:
|
||||
bool prepare_rgb565_scaling_(int width, int height);
|
||||
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
|
||||
int start_();
|
||||
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
|
||||
@@ -47,8 +49,13 @@ class JpegDecoder : public ImageDecoder {
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
JPEGDEC jpeg_{};
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
int source_width_{0};
|
||||
int source_height_{0};
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool direct_rgb565_{false};
|
||||
bool source_failed_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,15 +6,10 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image_decoder.png");
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
// Compressed bytes handed to pngle at a time
|
||||
static constexpr size_t FEED_PIECE_SIZE = 64;
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||
* data of the image is received (i.e. width and height);
|
||||
@@ -98,26 +93,16 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGD(TAG, "Waiting for more data");
|
||||
return 0;
|
||||
}
|
||||
// A few compressed bytes can hold many rows of pixels, so feed small pieces and stop once the call has run for
|
||||
// DECODE_TIME_LIMIT_MS; the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
const size_t piece = std::min(FEED_PIECE_SIZE, size - total);
|
||||
const int fed = pngle_feed(this->pngle_, buffer + total, piece);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
if (!this->size_valid_)
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
} else if (!this->size_valid_) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
} else {
|
||||
this->decoded_bytes_ += fed;
|
||||
total += fed;
|
||||
// pngle takes less than the piece when a chunk header or CRC runs past its end; that is no reason to stop
|
||||
if (fed == 0 || millis() - start >= DECODE_TIME_LIMIT_MS)
|
||||
break;
|
||||
}
|
||||
return total;
|
||||
return fed;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
@@ -104,17 +102,12 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
|
||||
Color color;
|
||||
const size_t total_pixels = this->width_ * this->height_;
|
||||
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
uint32_t steps = 0;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
if (++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS)
|
||||
break;
|
||||
color = this->last_pixel_;
|
||||
uint8_t byte = buffer[index];
|
||||
if (byte == QOI_OP_RGB) {
|
||||
if (size < index + QOI_RGB_CHUNK_SIZE) {
|
||||
break; // Need more data for RGB chunk
|
||||
return index; // Need more data for RGB chunk
|
||||
}
|
||||
index++;
|
||||
color.r = buffer[index++];
|
||||
@@ -124,7 +117,7 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
this->paint_index_++;
|
||||
} else if (byte == QOI_OP_RGBA) {
|
||||
if (size < index + QOI_RGBA_CHUNK_SIZE) {
|
||||
break; // Need more data for RGBA chunk
|
||||
return index; // Need more data for RGBA chunk
|
||||
}
|
||||
index++;
|
||||
color.r = buffer[index++];
|
||||
@@ -136,18 +129,15 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_RUN) {
|
||||
// QOI run chunk
|
||||
size_t run_length = (byte & QOI_MASK_VALUE) + 1; // run length is encoded in the lower 6 bits, plus one
|
||||
// Draw the part of the run in each row it covers at once
|
||||
while (run_length > 0) {
|
||||
const size_t x = this->paint_index_ % this->width_;
|
||||
const size_t count = std::min(run_length, this->width_ - x);
|
||||
this->draw(x, this->paint_index_ / this->width_, count, 1, color);
|
||||
this->paint_index_ += count;
|
||||
run_length -= count;
|
||||
for (size_t i = 0; i < run_length; i++) {
|
||||
// TODO: optimize by drawing runs of pixels at once instead of one by one
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
}
|
||||
index++;
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_LUMA) {
|
||||
if (size < index + QOI_LUMA_CHUNK_SIZE) {
|
||||
break; // Need more data for LUMA chunk
|
||||
return index; // Need more data for LUMA chunk
|
||||
}
|
||||
index++;
|
||||
uint8_t byte2 = buffer[index++];
|
||||
@@ -175,10 +165,8 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
this->last_pixel_ = color;
|
||||
this->color_table_[qoi_color_table_index(color)] = color;
|
||||
}
|
||||
if (this->paint_index_ >= total_pixels)
|
||||
index = size; // All that follows the last pixel is the end marker
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
this->decoded_bytes_ += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -113,9 +113,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
}
|
||||
case image::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
// 0.2125, 0.7154 and 0.0721 in 1/256ths, summing to 256 so white stays 255. Integer maths: the S3 has no
|
||||
// double FPU
|
||||
auto gray = static_cast<uint8_t>((54 * color.r + 183 * color.g + 19 * color.b) >> 8);
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import packet_interface
|
||||
from esphome.components.packet_interface import (
|
||||
CONF_MAX_PACKET_SIZE,
|
||||
new_packet_interface,
|
||||
packet_interface_schema,
|
||||
)
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_UDP_ID, UDP_SCHEMA, udp_ns
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DEPENDENCIES = ["udp"]
|
||||
|
||||
# Size of the receive buffer of the udp component
|
||||
MAX_PACKET_SIZE = 508
|
||||
|
||||
UdpPacketInterface = udp_ns.class_(
|
||||
"UdpPacketInterface",
|
||||
packet_interface.PacketInterface,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = packet_interface_schema(
|
||||
UdpPacketInterface, MAX_PACKET_SIZE, MAX_PACKET_SIZE
|
||||
).extend(UDP_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
udp_var = await cg.get_variable(config[CONF_UDP_ID])
|
||||
cg.add(udp_var.set_should_listen())
|
||||
# The udp component sends to its addresses through its broadcast socket
|
||||
cg.add(udp_var.set_should_broadcast())
|
||||
# The copy buffer's size is known here, so it is a static array rather than allocated at run time
|
||||
tx_buffer = f"{config[CONF_ID].id}__tx_buffer"
|
||||
cg.add_global(
|
||||
cg.RawStatement(f"static uint8_t {tx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
|
||||
)
|
||||
await new_packet_interface(config, udp_var, cg.RawExpression(tx_buffer))
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "udp_packet_interface.h"
|
||||
#ifdef USE_NETWORK
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
using packet_interface::PacketBuffer;
|
||||
using packet_interface::PacketMetaData;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "udp.packet_interface");
|
||||
|
||||
void UdpPacketInterface::setup() {
|
||||
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->receive_(data); });
|
||||
}
|
||||
|
||||
void UdpPacketInterface::receive_(std::span<const uint8_t> data) {
|
||||
if (data.size() > this->max_packet_size_) {
|
||||
ESP_LOGV(TAG, "%s: Packet of %zu bytes is larger than %zu, dropped", this->name_, data.size(),
|
||||
this->max_packet_size_);
|
||||
return;
|
||||
}
|
||||
this->on_receive_from_interface_(PacketBuffer(data.data(), data.size()), {});
|
||||
}
|
||||
|
||||
bool UdpPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
size_t size = data.size();
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "%s: Packet of %zu bytes is larger than %zu", this->name_, size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
if (!network::is_connected())
|
||||
return false;
|
||||
if (data.is_contiguous()) {
|
||||
auto span = data.as_span();
|
||||
return this->parent_->send_packet(span.data(), span.size());
|
||||
}
|
||||
data.copy_to(this->tx_buffer_.data(), size);
|
||||
return this->parent_->send_packet(this->tx_buffer_.data(), size);
|
||||
}
|
||||
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
// udp_component.h declares nothing without USE_NETWORK
|
||||
#ifdef USE_NETWORK
|
||||
#include <span>
|
||||
|
||||
#include "esphome/components/packet_interface/packet_interface.h"
|
||||
#include "../udp_component.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
/**
|
||||
* A packet interface link over UDP: each packet is one datagram, sent to every address of the UDP component.
|
||||
*
|
||||
* Every datagram received on the listen port is passed up, from any sender, so on a shared network an addressing
|
||||
* or encryption layer is needed to tell the peer's packets from others.
|
||||
*/
|
||||
class UdpPacketInterface : public packet_interface::PacketInterface {
|
||||
public:
|
||||
/// @param tx_buffer Joins packets split across buffers; its size is the largest packet, and it must outlive the link
|
||||
UdpPacketInterface(UDPComponent *udp_component, std::span<uint8_t> tx_buffer)
|
||||
: packet_interface::PacketInterface(tx_buffer.size()), parent_(udp_component), tx_buffer_(tx_buffer) {}
|
||||
|
||||
void setup() override;
|
||||
bool send_to_interface(const packet_interface::PacketBuffer &data,
|
||||
const packet_interface::PacketMetaData &meta_data) override;
|
||||
|
||||
protected:
|
||||
void receive_(std::span<const uint8_t> data);
|
||||
|
||||
UDPComponent *parent_;
|
||||
const std::span<uint8_t> tx_buffer_;
|
||||
};
|
||||
} // namespace esphome::udp
|
||||
#endif
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "udp_transport.h"
|
||||
|
||||
namespace esphome::udp {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "udp_transport");
|
||||
|
||||
bool UDPTransport::should_send() { return network::is_connected(); }
|
||||
void UDPTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
|
||||
@@ -205,25 +205,18 @@ void UDPComponent::set_poll_warning_(PollStep step, const LogString *what) {
|
||||
}
|
||||
|
||||
void UDPComponent::loop() {
|
||||
if (!this->should_listen_) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
// One byte more than the largest packet, as a read cuts a longer datagram to fit and so cannot report it
|
||||
std::array<uint8_t, MAX_PACKET_SIZE + 1> buf;
|
||||
for (;;) {
|
||||
if (!this->listen_socket_->ready())
|
||||
break;
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
if (len <= 0)
|
||||
break;
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
if (packet_len > MAX_PACKET_SIZE) {
|
||||
ESP_LOGV(TAG, "Packet longer than %zu bytes dropped", MAX_PACKET_SIZE);
|
||||
continue;
|
||||
if (this->should_listen_) {
|
||||
std::array<uint8_t, MAX_PACKET_SIZE> buf;
|
||||
for (;;) {
|
||||
if (!this->listen_socket_->ready())
|
||||
break;
|
||||
auto len = this->listen_socket_->read(buf.data(), buf.size());
|
||||
if (len <= 0)
|
||||
break;
|
||||
size_t packet_len = static_cast<size_t>(len);
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||
}
|
||||
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
|
||||
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +239,9 @@ void UDPComponent::dump_config() {
|
||||
YESNO(this->should_broadcast_), YESNO(this->should_listen_));
|
||||
}
|
||||
|
||||
bool UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
if (this->is_failed())
|
||||
return false; // a send socket may be missing
|
||||
bool sent = true;
|
||||
return; // a send socket may be missing
|
||||
for (const auto &entry : this->sockaddrs_) {
|
||||
#ifdef USE_UDP_IPV6_SEND
|
||||
const bool v6 = entry.sa.sa_family == AF_INET6;
|
||||
@@ -264,23 +256,11 @@ bool UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
socket::UDPSendSocket *sock = this->broadcast_socket_.get();
|
||||
socklen_t sockaddr_size = sizeof(sockaddr_in);
|
||||
#endif
|
||||
if (sock == nullptr) {
|
||||
ESP_LOGW(TAG, "Broadcast socket not initialized");
|
||||
return false;
|
||||
}
|
||||
auto result = sock->sendto(data, size, 0, &entry.sa, sockaddr_size);
|
||||
if (result < 0) {
|
||||
// lwIP refuses a packet with ENOMEM while the destination's ARP lookup is pending and its queue is full;
|
||||
// the packet can be sent again later
|
||||
if (errno == ENOMEM) {
|
||||
ESP_LOGV(TAG, "sendto() out of memory");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
sent = false;
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
} // namespace esphome::udp
|
||||
|
||||
|
||||
@@ -35,9 +35,8 @@ class UDPComponent final : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
/// Returns false if the packet could not be sent to every destination.
|
||||
bool send_packet(const uint8_t *data, size_t size);
|
||||
bool send_packet(const std::vector<uint8_t> &buf) { return this->send_packet(buf.data(), buf.size()); }
|
||||
void send_packet(const uint8_t *data, size_t size);
|
||||
void send_packet(const std::vector<uint8_t> &buf) { this->send_packet(buf.data(), buf.size()); }
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; };
|
||||
|
||||
protected:
|
||||
@@ -54,7 +53,7 @@ class UDPComponent final : public Component {
|
||||
bool network_poll_();
|
||||
void set_poll_warning_(PollStep step, const LogString *what);
|
||||
|
||||
LazyCallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
|
||||
std::unique_ptr<socket::UDPSendSocket> broadcast_socket_ = nullptr;
|
||||
std::unique_ptr<socket::UDPSocket> listen_socket_ = nullptr;
|
||||
#ifdef USE_UDP_IPV6_SEND
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
from esphome import automation, preferences
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_ENABLED, CONF_MODEM, CONF_USE_PSRAM
|
||||
from esphome.components.const import CONF_ENABLED, CONF_USE_PSRAM
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
const,
|
||||
@@ -461,15 +461,11 @@ def final_validate(config):
|
||||
|
||||
ap_is_wisp = has_ap and config[CONF_AP][CONF_AP_MODE] == WIFI_AP_MODE_WISP
|
||||
if ap_is_wisp:
|
||||
# The clients are routed through the station, or through ethernet or modem
|
||||
if (
|
||||
not has_sta
|
||||
and CONF_ETHERNET not in full_config
|
||||
and CONF_MODEM not in full_config
|
||||
):
|
||||
# The clients are routed through the station, or through ethernet
|
||||
if not has_sta and CONF_ETHERNET not in full_config:
|
||||
raise cv.Invalid(
|
||||
"WISP mode needs a station network (wifi.ssid or wifi.networks), "
|
||||
"or ethernet or modem as the upstream interface"
|
||||
"or ethernet as the upstream interface"
|
||||
)
|
||||
if has_captive_portal:
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -129,9 +129,8 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
|
||||
memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t));
|
||||
#endif
|
||||
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
|
||||
} else if (event_base == IP_EVENT &&
|
||||
(event_id == IP_EVENT_ETH_GOT_IP || event_id == IP_EVENT_PPP_GOT_IP)) { // NOLINT(bugprone-branch-clone)
|
||||
// no data: the bridge rereads the resolvers an ethernet or modem upstream just received
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data: the bridge rereads the resolvers an ethernet upstream just received
|
||||
#endif
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data
|
||||
@@ -1129,8 +1128,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
#endif
|
||||
|
||||
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
|
||||
} else if (data->event_base == IP_EVENT &&
|
||||
(data->event_id == IP_EVENT_ETH_GOT_IP || data->event_id == IP_EVENT_PPP_GOT_IP)) {
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ETH_GOT_IP) {
|
||||
this->apply_ap_dns_();
|
||||
#endif
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
#define USE_GPIO_SWITCH_INTERLOCK
|
||||
#define USE_GRAPH
|
||||
#define USE_GRAPHICAL_DISPLAY_MENU
|
||||
#define USE_GREE_YAW1F
|
||||
#define USE_HOERMANN_HCP_IDENTITY
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
#define USE_HOMEASSISTANT_TIMEZONE
|
||||
@@ -274,9 +273,6 @@
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OUTPUT
|
||||
#define USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
#define USE_PACKET_INTERFACE_BASIC_SESSION
|
||||
#define USE_PACKET_INTERFACE_HDLC
|
||||
#define USE_PACKET_INTERFACE_XXTEA
|
||||
#define USE_PCA9554_PCAL
|
||||
#define USE_POWER_SUPPLY
|
||||
// Only defined by key-lookup preference backends; the slot-based platforms
|
||||
@@ -630,11 +626,8 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#define USE_MODEM
|
||||
#define USE_MODEM_NETWORK_TYPE
|
||||
#define USE_MODEM_PIN_CODE
|
||||
#define USE_MODEM_POLL
|
||||
#define USE_MODEM_SEND_AT
|
||||
#define USE_MODEM_SIGNAL
|
||||
#define USE_MODEM_URC
|
||||
#endif
|
||||
|
||||
|
||||
+30
-88
@@ -5,7 +5,6 @@ from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ssl
|
||||
import tempfile
|
||||
import time
|
||||
@@ -24,7 +23,6 @@ from esphome.const import (
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_MQTT,
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
CONF_PASSWORD,
|
||||
CONF_PAYLOAD,
|
||||
CONF_PORT,
|
||||
@@ -36,7 +34,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import get_int_env, get_str_env
|
||||
from esphome.mqtt_topics import per_board_suffix
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import safe_print
|
||||
|
||||
@@ -397,101 +394,48 @@ def show_logs(
|
||||
password: str | None = None,
|
||||
client_id: str | None = None,
|
||||
) -> int:
|
||||
# Where the device publishes its logs, also needed to match a topic given on the command line
|
||||
config_topic: str | None = None
|
||||
error: str | None = None
|
||||
mqtt_conf = config.get(CONF_MQTT, {})
|
||||
if CONF_MQTT not in config:
|
||||
error = "MQTT isn't setup, can't start MQTT logs"
|
||||
elif CONF_LOG_TOPIC in mqtt_conf:
|
||||
if mqtt_conf[CONF_LOG_TOPIC] is None:
|
||||
error = "MQTT log topic set to null, can't start MQTT logs"
|
||||
elif CONF_TOPIC not in mqtt_conf[CONF_LOG_TOPIC]:
|
||||
error = "MQTT log topic not available, can't start MQTT logs"
|
||||
if topic is not None:
|
||||
pass # already have topic
|
||||
elif CONF_MQTT in config:
|
||||
conf = config[CONF_MQTT]
|
||||
if CONF_LOG_TOPIC in conf:
|
||||
if config[CONF_MQTT][CONF_LOG_TOPIC] is None:
|
||||
_LOGGER.error("MQTT log topic set to null, can't start MQTT logs")
|
||||
return 1
|
||||
if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]:
|
||||
_LOGGER.error("MQTT log topic not available, can't start MQTT logs")
|
||||
return 1
|
||||
topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC]
|
||||
elif CONF_TOPIC_PREFIX in config[CONF_MQTT]:
|
||||
topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug"
|
||||
else:
|
||||
config_topic = mqtt_conf[CONF_LOG_TOPIC][CONF_TOPIC]
|
||||
elif CONF_TOPIC_PREFIX in mqtt_conf:
|
||||
config_topic = f"{mqtt_conf[CONF_TOPIC_PREFIX]}/debug"
|
||||
topic = f"{config[CONF_ESPHOME][CONF_NAME]}/debug"
|
||||
else:
|
||||
config_topic = f"{config[CONF_ESPHOME][CONF_NAME]}/debug"
|
||||
# A topic given on the command line is followed as written
|
||||
topic_from_cli = topic is not None
|
||||
if not topic_from_cli:
|
||||
if error is not None:
|
||||
_LOGGER.error(error)
|
||||
return 1
|
||||
topic = config_topic
|
||||
esphome_conf = config[CONF_ESPHOME]
|
||||
name = esphome_conf[CONF_NAME]
|
||||
# With the MAC suffix every board listens on esphome/logs/<name>-<mac>, which only its birth message names
|
||||
mac_suffix = esphome_conf.get(CONF_NAME_ADD_MAC_SUFFIX, False)
|
||||
_LOGGER.error("MQTT isn't setup, can't start MQTT logs")
|
||||
return 1
|
||||
_LOGGER.info("Starting log output from %s", topic)
|
||||
|
||||
logs_topic = f"esphome/logs/{config[CONF_ESPHOME][CONF_NAME]}"
|
||||
# Ask when the retained birth message reports online: a request sent while the device is down is
|
||||
# lost, and only a retained message tells a CLI that connects later that the device is already up.
|
||||
# An empty payload deletes the retained message instead, so it counts as no birth message.
|
||||
birth = mqtt_conf.get(CONF_BIRTH_MESSAGE) or {}
|
||||
birth = config.get(CONF_MQTT, {}).get(CONF_BIRTH_MESSAGE) or {}
|
||||
birth_payload = birth.get(CONF_PAYLOAD)
|
||||
birth_topic = birth.get(CONF_TOPIC) if birth_payload else None
|
||||
ask_on_connect = not mac_suffix and (birth_topic is None or not birth[CONF_RETAIN])
|
||||
ask_on_connect = birth_topic is None or not birth[CONF_RETAIN]
|
||||
|
||||
def per_board(present: str | None, key: str) -> re.Pattern[str] | None:
|
||||
# Topics the firmware builds from <name>-<mac>, which the CLI cannot know
|
||||
if present is None or not (
|
||||
suffix := per_board_suffix(mqtt_conf, key, name, mac_suffix)
|
||||
):
|
||||
return None
|
||||
return re.compile(rf"({re.escape(name)}-[0-9a-f]{{6}}){re.escape(suffix)}")
|
||||
|
||||
board_log_pattern = per_board(config_topic, CONF_LOG_TOPIC)
|
||||
board_logs = board_log_pattern is not None
|
||||
log_pattern = None if topic_from_cli else board_log_pattern
|
||||
birth_pattern = per_board(birth_topic, CONF_BIRTH_MESSAGE)
|
||||
_LOGGER.info(
|
||||
"Starting log output from %s",
|
||||
f"{name}-<mac>/debug of every device built from this config"
|
||||
if log_pattern
|
||||
else topic,
|
||||
)
|
||||
|
||||
def request_crash_log(client: mqtt.Client, board: str = name) -> None:
|
||||
logs_topic = f"esphome/logs/{board}"
|
||||
def request_crash_log(client: mqtt.Client) -> None:
|
||||
_LOGGER.debug("Requesting the crash log via topic: %s", logs_topic)
|
||||
client.publish(logs_topic, None, retain=False)
|
||||
|
||||
def shows_board_logs(board: str) -> bool:
|
||||
board_topic = f"{board}/debug" if board_logs else config_topic
|
||||
return board_topic is not None and mqtt.topic_matches_sub(topic, board_topic)
|
||||
|
||||
def board_to_ask(birth_msg_topic: str) -> str | None:
|
||||
if birth_pattern is None:
|
||||
return name if birth_msg_topic == birth_topic and not mac_suffix else None
|
||||
if (match := birth_pattern.fullmatch(birth_msg_topic)) and (
|
||||
# Replaying clears the stored crash, so with a --topic only ask the boards it shows
|
||||
not topic_from_cli or shows_board_logs(match[1])
|
||||
):
|
||||
return match[1]
|
||||
return None
|
||||
|
||||
def on_message(client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage) -> None:
|
||||
t = msg.topic
|
||||
# The birth topic may sit inside the log subscription, so print by what was asked for
|
||||
shown = (
|
||||
log_pattern.fullmatch(t)
|
||||
if log_pattern
|
||||
else mqtt.topic_matches_sub(topic, t)
|
||||
)
|
||||
board = board_to_ask(t)
|
||||
if not (shown or board):
|
||||
return
|
||||
payload = msg.payload.decode(errors="backslashreplace")
|
||||
if board and payload == birth_payload:
|
||||
request_crash_log(client, board)
|
||||
if not shown:
|
||||
return
|
||||
if log_pattern:
|
||||
# Several boards can run this config, so name the one that logged
|
||||
payload = f"[{shown[1]}]{payload}"
|
||||
time_ = datetime.now().astimezone().time().strftime("[%H:%M:%S]")
|
||||
safe_print(time_ + payload)
|
||||
if msg.topic == birth_topic and payload == birth_payload:
|
||||
request_crash_log(client)
|
||||
# The birth topic may sit inside the log subscription, so print by what was asked for
|
||||
if mqtt.topic_matches_sub(topic, msg.topic):
|
||||
time_ = datetime.now().astimezone().time().strftime("[%H:%M:%S]")
|
||||
safe_print(time_ + payload)
|
||||
|
||||
def on_connect(
|
||||
client: mqtt.Client,
|
||||
@@ -503,9 +447,7 @@ def show_logs(
|
||||
if ask_on_connect:
|
||||
request_crash_log(client)
|
||||
|
||||
subscriptions = ["+/debug" if log_pattern else topic]
|
||||
if birth_topic is not None:
|
||||
subscriptions.append("+/status" if birth_pattern else birth_topic)
|
||||
subscriptions = [topic] if birth_topic is None else [topic, birth_topic]
|
||||
return initialize(
|
||||
config, subscriptions, on_message, on_connect, username, password, client_id
|
||||
)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""MQTT topic rules shared by code generation and the MQTT log viewer."""
|
||||
|
||||
from esphome.const import (
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_SHUTDOWN_MESSAGE,
|
||||
CONF_TOPIC,
|
||||
CONF_TOPIC_PREFIX,
|
||||
CONF_WILL_MESSAGE,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# Topic suffix of each message whose topic defaults to one under the topic prefix
|
||||
DEFAULT_TOPIC_SUFFIXES = {
|
||||
CONF_BIRTH_MESSAGE: "/status",
|
||||
CONF_WILL_MESSAGE: "/status",
|
||||
CONF_SHUTDOWN_MESSAGE: "/status",
|
||||
CONF_LOG_TOPIC: "/debug",
|
||||
}
|
||||
|
||||
|
||||
def prefix_gets_mac_suffix(topic_prefix: str, name: str, mac_suffix: bool) -> bool:
|
||||
"""Whether the firmware appends the MAC suffix to this topic prefix at runtime."""
|
||||
return mac_suffix and topic_prefix == name
|
||||
|
||||
|
||||
def per_board_suffix(
|
||||
mqtt_config: ConfigType, key: str, name: str, mac_suffix: bool
|
||||
) -> str | None:
|
||||
"""Topic suffix of a message the firmware sends on App.get_name() + suffix, else None.
|
||||
|
||||
A message missing from the config counts as the one defaulted from the topic prefix.
|
||||
"""
|
||||
topic_prefix = mqtt_config.get(CONF_TOPIC_PREFIX, name)
|
||||
if not prefix_gets_mac_suffix(topic_prefix, name, mac_suffix):
|
||||
return None
|
||||
suffix = DEFAULT_TOPIC_SUFFIXES[key]
|
||||
if (
|
||||
key in mqtt_config
|
||||
and (mqtt_config[key] or {}).get(CONF_TOPIC) != f"{topic_prefix}{suffix}"
|
||||
):
|
||||
return None
|
||||
return suffix
|
||||
Whitespace-only changes.
@@ -1,74 +0,0 @@
|
||||
"""Tests for the es7243 check that its I2S bus drives MCLK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.es7243.audio_adc import FINAL_VALIDATE_SCHEMA
|
||||
from esphome.components.i2s_audio import CONF_I2S_AUDIO_ID, CONF_I2S_MCLK_PIN
|
||||
from esphome.config import Config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, PlatformFramework
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
BUS_ID = "i2s_audio_bus"
|
||||
OTHER_BUS_ID = "i2s_audio_other"
|
||||
|
||||
|
||||
def _bus(name: str, *, mclk: bool) -> ConfigType:
|
||||
bus: ConfigType = {CONF_ID: ID(name, is_declaration=True, type="I2SAudioComponent")}
|
||||
if mclk:
|
||||
bus[CONF_I2S_MCLK_PIN] = 0
|
||||
return bus
|
||||
|
||||
|
||||
def _full_config(*buses: ConfigType) -> Config:
|
||||
"""A full config carrying the given i2s_audio buses, with their ids declared as the ID pass does."""
|
||||
full = Config()
|
||||
full["i2s_audio"] = list(buses)
|
||||
for index, bus in enumerate(buses):
|
||||
full.declare_ids.append((bus[CONF_ID], ["i2s_audio", index, CONF_ID]))
|
||||
return full
|
||||
|
||||
|
||||
def _adc(bus_id: str) -> ConfigType:
|
||||
return {
|
||||
CONF_I2S_AUDIO_ID: ID(bus_id, is_declaration=False, type="I2SAudioComponent")
|
||||
}
|
||||
|
||||
|
||||
def test_bus_with_mclk_passes(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, full_config=_full_config(_bus(BUS_ID, mclk=True))
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA(_adc(BUS_ID))
|
||||
|
||||
|
||||
def test_bus_without_mclk_is_rejected(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, full_config=_full_config(_bus(BUS_ID, mclk=False))
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
cv.Invalid, match=f"es7243 needs {CONF_I2S_MCLK_PIN} on i2s_audio {BUS_ID}"
|
||||
):
|
||||
FINAL_VALIDATE_SCHEMA(_adc(BUS_ID))
|
||||
|
||||
|
||||
def test_only_the_selected_bus_is_checked(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Another bus without MCLK does not matter, and one with MCLK does not help."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
full_config=_full_config(
|
||||
_bus(BUS_ID, mclk=True), _bus(OTHER_BUS_ID, mclk=False)
|
||||
),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA(_adc(BUS_ID))
|
||||
with pytest.raises(cv.Invalid, match=OTHER_BUS_ID):
|
||||
FINAL_VALIDATE_SCHEMA(_adc(OTHER_BUS_ID))
|
||||
@@ -7,7 +7,6 @@ from unittest import mock
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.const import CONF_DEVICE_TYPE
|
||||
from esphome.components.esp32 import (
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
@@ -26,7 +25,7 @@ from esphome.components.esp32_power_management.power_management import (
|
||||
to_code,
|
||||
valid_min_frequencies,
|
||||
)
|
||||
from esphome.components.openthread.const import CONF_POLL_PERIOD
|
||||
from esphome.components.openthread.const import CONF_DEVICE_TYPE, CONF_POLL_PERIOD
|
||||
from esphome.const import CONF_OPENTHREAD, KEY_FRAMEWORK_VERSION, PlatformFramework
|
||||
from esphome.core import CORE, TimePeriodMilliseconds
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Gree climate regression tests."""
|
||||
@@ -1,16 +0,0 @@
|
||||
esphome:
|
||||
name: gree-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
remote_transmitter:
|
||||
id: xmitr
|
||||
pin: GPIO4
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE YAN
|
||||
model: yan
|
||||
transmitter_id: xmitr
|
||||
@@ -1,16 +0,0 @@
|
||||
esphome:
|
||||
name: gree-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
remote_transmitter:
|
||||
id: xmitr
|
||||
pin: GPIO4
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE YAW1F
|
||||
model: yaw1f
|
||||
transmitter_id: xmitr
|
||||
@@ -1,22 +0,0 @@
|
||||
"""The YAW1F follow-up message is compiled in only for configs that use that model."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_yaw1f_model_emits_its_define(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("yaw1f.yaml"))
|
||||
assert "USE_GREE_YAW1F" in {define.name for define in CORE.defines}
|
||||
|
||||
|
||||
def test_other_models_leave_the_define_out(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
generate_main(component_config_path("yan.yaml"))
|
||||
assert "USE_GREE_YAW1F" not in {define.name for define in CORE.defines}
|
||||
@@ -1,12 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
max_concurrent_requests: 3
|
||||
@@ -1,20 +0,0 @@
|
||||
"""max_concurrent_requests is set only when it differs from the default of 1."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_max_concurrent_requests_default_is_not_set(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("test_esp32_stock.yaml"))
|
||||
assert "set_max_concurrent_requests" not in main_cpp
|
||||
|
||||
|
||||
def test_max_concurrent_requests_is_set(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("max_concurrent.yaml"))
|
||||
assert "->set_max_concurrent_requests(3);" in main_cpp
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
topic_prefix: custom
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
log_topic: test-device/debug
|
||||
birth_message:
|
||||
topic: test-device/status
|
||||
payload: online
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
log_topic: test-device/log
|
||||
birth_message:
|
||||
topic: test-device
|
||||
payload: online
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
topic_prefix:
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Tests for MQTT topic generation with name_add_mac_suffix."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "present", "absent"),
|
||||
[
|
||||
pytest.param(
|
||||
"mac_suffix_default_topics.yaml",
|
||||
(
|
||||
"set_topic_prefix(App.get_name())",
|
||||
'(App.get_name() + "/status")',
|
||||
'(App.get_name() + "/debug")',
|
||||
),
|
||||
('"test-device/status"', '"test-device/debug"'),
|
||||
id="default topics follow the MAC suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"mac_suffix_explicit_topics.yaml",
|
||||
(
|
||||
"set_topic_prefix(App.get_name())",
|
||||
# will and shutdown are still defaulted from the prefix
|
||||
'(App.get_name() + "/status")',
|
||||
'.topic = "test-device/log",',
|
||||
'.topic = "test-device",',
|
||||
),
|
||||
('App.get_name() + "/log"', 'App.get_name() + "/debug"'),
|
||||
id="explicit topics stay literal",
|
||||
),
|
||||
pytest.param(
|
||||
"mac_suffix_default_written_out.yaml",
|
||||
('(App.get_name() + "/status")', '(App.get_name() + "/debug")'),
|
||||
('"test-device/status"', '"test-device/debug"'),
|
||||
id="topics written out as the default follow the MAC suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"mac_suffix_custom_prefix.yaml",
|
||||
('set_topic_prefix("custom")', '"custom/status"', '"custom/debug"'),
|
||||
("App.get_name()",),
|
||||
id="custom prefix stays literal",
|
||||
),
|
||||
pytest.param(
|
||||
"no_mac_suffix.yaml",
|
||||
(
|
||||
'set_topic_prefix("test-device")',
|
||||
'"test-device/status"',
|
||||
'"test-device/debug"',
|
||||
),
|
||||
("App.get_name()",),
|
||||
id="no MAC suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"no_topic_prefix.yaml",
|
||||
(
|
||||
'set_topic_prefix("")',
|
||||
"disable_birth_message()",
|
||||
"disable_last_will()",
|
||||
"disable_shutdown_message()",
|
||||
"disable_log_message()",
|
||||
),
|
||||
(),
|
||||
id="null prefix disables default messages",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_topics(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
present: tuple[str, ...],
|
||||
absent: tuple[str, ...],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
for text in present:
|
||||
assert text in main_cpp
|
||||
for text in absent:
|
||||
assert text not in main_cpp
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.const import CONF_BORDER_ROUTER, CONF_DEVICE_TYPE
|
||||
from esphome.components.const import CONF_BORDER_ROUTER
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
@@ -27,7 +27,7 @@ from esphome.components.openthread import (
|
||||
_validate,
|
||||
_validate_platform,
|
||||
)
|
||||
from esphome.components.openthread.const import CONF_RCP
|
||||
from esphome.components.openthread.const import CONF_DEVICE_TYPE, CONF_RCP
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""transmit_pronto only sends data; delta is a receive-side match tolerance."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components.remote_base import PRONTO_BINARY_SENSOR_SCHEMA
|
||||
import esphome.components.remote_transmitter # noqa: F401
|
||||
|
||||
PRONTO_DATA = "0000 006D 0001 0000 0015 0016"
|
||||
|
||||
|
||||
def test_transmit_rejects_delta() -> None:
|
||||
with pytest.raises(cv.Invalid, match="delta"):
|
||||
ACTION_REGISTRY["remote_transmitter.transmit_pronto"].schema(
|
||||
{"data": PRONTO_DATA, "delta": 10}
|
||||
)
|
||||
|
||||
|
||||
def test_transmit_accepts_data() -> None:
|
||||
config = ACTION_REGISTRY["remote_transmitter.transmit_pronto"].schema(
|
||||
{"data": PRONTO_DATA}
|
||||
)
|
||||
assert config["data"] == PRONTO_DATA
|
||||
|
||||
|
||||
def test_binary_sensor_schema_keeps_delta() -> None:
|
||||
config = PRONTO_BINARY_SENSOR_SCHEMA({"data": PRONTO_DATA, "delta": 10})
|
||||
assert config["delta"] == 10
|
||||
@@ -1,6 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The driver lives in the sensor platform, so pull in what it builds against.
|
||||
manifest.dependencies = manifest.dependencies + ["i2c", "sensor"]
|
||||
@@ -1,57 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/as7341/as7341.h"
|
||||
|
||||
namespace esphome::as7341::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x39;
|
||||
static constexpr uint8_t SMUXEN = 1 << 4;
|
||||
static constexpr uint8_t AVALID = 1 << 6;
|
||||
static constexpr uint8_t NUM_ADC_CHANNELS = 6;
|
||||
|
||||
// The chip's registers behind an address pointer that moves on after every byte.
|
||||
// SMUX commands finish at once and spectral data is always valid.
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (size != 0) {
|
||||
this->pointer_ = data[0];
|
||||
for (size_t i = 1; i < size; i++)
|
||||
this->registers[this->pointer_++] = data[i];
|
||||
}
|
||||
this->registers[AS7341_ENABLE] &= ~SMUXEN;
|
||||
for (size_t i = 0; i < out_size; i++)
|
||||
out[i] = this->registers[this->pointer_++];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 256> registers{};
|
||||
|
||||
protected:
|
||||
uint8_t pointer_{0};
|
||||
};
|
||||
|
||||
TEST(AS7341Test, ReadChannelsDecodesLowByteFirst) {
|
||||
RegisterBus bus;
|
||||
AS7341Component sensor;
|
||||
sensor.set_i2c_bus(&bus);
|
||||
sensor.set_i2c_address(ADDRESS);
|
||||
bus.registers[AS7341_STATUS2] = AVALID;
|
||||
|
||||
// CHn_DATA_L holds the low byte and CHn_DATA_H, one address up, the high byte.
|
||||
for (uint8_t channel = 0; channel < NUM_ADC_CHANNELS; channel++) {
|
||||
bus.registers[AS7341_CH0_DATA_L + 2 * channel] = 0x34 + channel;
|
||||
bus.registers[AS7341_CH0_DATA_H + 2 * channel] = 0x12;
|
||||
}
|
||||
|
||||
std::array<uint16_t, 2 * NUM_ADC_CHANNELS> data{};
|
||||
ASSERT_TRUE(sensor.read_channels(data.data()));
|
||||
for (size_t i = 0; i < data.size(); i++)
|
||||
EXPECT_EQ(data[i], 0x1234 + i % NUM_ADC_CHANNELS) << "value " << unsigned(i);
|
||||
}
|
||||
|
||||
} // namespace esphome::as7341::testing
|
||||
@@ -1,15 +0,0 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- audio_adc.set_mic_gain:
|
||||
id: es7243_adc
|
||||
mic_gain: 1db
|
||||
- audio_adc.set_mic_gain:
|
||||
id: es7243_adc
|
||||
mic_gain: !lambda 'return 18;'
|
||||
|
||||
audio_adc:
|
||||
- platform: es7243
|
||||
id: es7243_adc
|
||||
i2c_id: i2c_bus
|
||||
i2s_audio_id: i2s_audio_bus
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml
|
||||
es7243: !include common.yaml
|
||||
@@ -1,14 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# AUTO_LOAD sits on the climate platform, which the unit test build does not load.
|
||||
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate"]
|
||||
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
# The YAW1F follow-up message is compiled in only for configs that use that model
|
||||
cg.add_define("USE_GREE_YAW1F")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -1,8 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include "esphome/components/gree/gree.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome::gree::testing {
|
||||
|
||||
@@ -24,209 +21,4 @@ TEST(GreeClimateTest, HeatCoolOverrideAdvertisedWithoutHeat) {
|
||||
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
|
||||
}
|
||||
|
||||
class CapturingGreeTransmitter final : public remote_base::RemoteTransmitterBase {
|
||||
public:
|
||||
CapturingGreeTransmitter() : RemoteTransmitterBase(nullptr) {}
|
||||
|
||||
std::array<uint8_t, 8> frame() const {
|
||||
const auto &raw = this->captured_;
|
||||
EXPECT_EQ(raw.size(), 140U);
|
||||
std::array<uint8_t, 8> state{};
|
||||
if (raw.size() != 140)
|
||||
return state;
|
||||
|
||||
// Two header timings, 64 timings per data half and eight separator/gap timings.
|
||||
for (size_t byte = 0; byte < 8; byte++) {
|
||||
const size_t start = byte < 4 ? 2 + byte * 16 : 74 + (byte - 4) * 16;
|
||||
for (size_t bit = 0; bit < 8; bit++) {
|
||||
const int32_t space = raw[start + bit * 2 + 1];
|
||||
EXPECT_TRUE(space == -static_cast<int32_t>(GREE_ZERO_SPACE) || space == -static_cast<int32_t>(GREE_ONE_SPACE));
|
||||
if (space == -static_cast<int32_t>(GREE_ONE_SPACE))
|
||||
state[byte] |= 1U << bit;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
protected:
|
||||
void send_internal(uint32_t /*send_times*/, uint32_t /*send_wait*/) override {
|
||||
this->captured_ = this->temp_.get_data();
|
||||
}
|
||||
|
||||
private:
|
||||
remote_base::RawTimings captured_;
|
||||
};
|
||||
|
||||
static std::array<uint8_t, 8> transmit_test_frame(Model model, climate::ClimateMode mode, climate::ClimateFanMode fan,
|
||||
climate::ClimateSwingMode swing) {
|
||||
CapturingGreeTransmitter transmitter;
|
||||
GreeClimate device;
|
||||
device.set_model(model);
|
||||
device.set_transmitter(&transmitter);
|
||||
auto call = device.make_call();
|
||||
call.set_mode(mode);
|
||||
call.set_target_temperature(22);
|
||||
call.set_fan_mode(fan);
|
||||
call.set_swing_mode(swing);
|
||||
call.perform();
|
||||
return transmitter.frame();
|
||||
}
|
||||
|
||||
TEST(GreeTxRegression, YX1FFHighUsesFan3AndTurboWithoutSleep) {
|
||||
const std::array<uint8_t, 8> expected{0x39, 0x06, 0x70, 0x50, 0x00, 0x20, 0x00, 0xB0};
|
||||
EXPECT_EQ(transmit_test_frame(GREE_YX1FF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_SWING_OFF),
|
||||
expected);
|
||||
}
|
||||
|
||||
TEST(GreeTxRegression, YX1FFMediumUsesFan3WithoutTurbo) {
|
||||
const std::array<uint8_t, 8> expected{0x39, 0x06, 0x60, 0x50, 0x00, 0x20, 0x00, 0xB0};
|
||||
EXPECT_EQ(transmit_test_frame(GREE_YX1FF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_SWING_OFF),
|
||||
expected);
|
||||
}
|
||||
|
||||
TEST(GreeTxRegression, YX1FFVerticalSwingUsesBothVaneNibbles) {
|
||||
const std::array<uint8_t, 8> expected{0x79, 0x06, 0x70, 0x50, 0x11, 0x20, 0x00, 0xC0};
|
||||
EXPECT_EQ(transmit_test_frame(GREE_YX1FF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_SWING_VERTICAL),
|
||||
expected);
|
||||
}
|
||||
|
||||
// Power off only clears ModelA; Turbo and the fan speed stay latched, as in IRremoteESP8266's IRGreeAC::setPower()
|
||||
TEST(GreeTxRegression, YX1FFPowerOffClearsModelABit) {
|
||||
const std::array<uint8_t, 8> expected{0x30, 0x06, 0x30, 0x50, 0x00, 0x20, 0x00, 0x20};
|
||||
EXPECT_EQ(
|
||||
transmit_test_frame(GREE_YX1FF, climate::CLIMATE_MODE_OFF, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_SWING_OFF),
|
||||
expected);
|
||||
}
|
||||
|
||||
TEST(GreeTxRegression, YX1FFDoesNotAdvertiseHorizontalSwing) {
|
||||
GreeClimate device;
|
||||
device.set_model(GREE_YX1FF);
|
||||
const auto traits = device.get_traits();
|
||||
EXPECT_TRUE(traits.supports_swing_mode(climate::CLIMATE_SWING_VERTICAL));
|
||||
EXPECT_FALSE(traits.supports_swing_mode(climate::CLIMATE_SWING_HORIZONTAL));
|
||||
EXPECT_FALSE(traits.supports_swing_mode(climate::CLIMATE_SWING_BOTH));
|
||||
}
|
||||
|
||||
TEST(GreeTxRegression, YACVerticalVaneIsInLowNibble) {
|
||||
const std::array<uint8_t, 8> expected{0x49, 0x06, 0x20, 0x50, 0x01, 0x20, 0x20, 0xD0};
|
||||
EXPECT_EQ(transmit_test_frame(GREE_YAC, climate::CLIMATE_MODE_COOL, climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_VERTICAL),
|
||||
expected);
|
||||
}
|
||||
|
||||
TEST(GreeTxRegression, YACCombinedSwingUsesBothNibbles) {
|
||||
const std::array<uint8_t, 8> expected{0x49, 0x06, 0x20, 0x50, 0x11, 0x20, 0x20, 0xE0};
|
||||
EXPECT_EQ(
|
||||
transmit_test_frame(GREE_YAC, climate::CLIMATE_MODE_COOL, climate::CLIMATE_FAN_AUTO, climate::CLIMATE_SWING_BOTH),
|
||||
expected);
|
||||
}
|
||||
|
||||
class CapturingTransmitter : public remote_base::RemoteTransmitterBase {
|
||||
public:
|
||||
CapturingTransmitter() : RemoteTransmitterBase(nullptr) {}
|
||||
const remote_base::RemoteTransmitData &data() const { return this->temp_; }
|
||||
uint32_t send_count{0};
|
||||
|
||||
protected:
|
||||
void send_internal(uint32_t send_times, uint32_t send_wait) override { this->send_count += send_times; }
|
||||
};
|
||||
|
||||
// Expected bytes are fixed vectors, not calculated with the encoder's checksum or model constants.
|
||||
static void append_frame(remote_base::RawTimings &timings, const std::array<uint8_t, 8> &bytes, int32_t header_space,
|
||||
int32_t message_space) {
|
||||
timings.push_back(9000);
|
||||
timings.push_back(-header_space);
|
||||
for (size_t i = 0; i < bytes.size(); i++) {
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
timings.push_back(620);
|
||||
timings.push_back(bytes[i] & (1 << bit) ? -1600 : -540);
|
||||
}
|
||||
if (i == 3) {
|
||||
timings.insert(timings.end(), {620, -540, 620, -1600, 620, -540, 620, -message_space});
|
||||
}
|
||||
}
|
||||
timings.push_back(620);
|
||||
}
|
||||
|
||||
class GreeTransmitTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->climate_.set_transmitter(&this->transmitter_);
|
||||
this->climate_.mode = climate::CLIMATE_MODE_OFF;
|
||||
this->climate_.target_temperature = 29;
|
||||
this->climate_.fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
this->climate_.swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
void transmit_(Model model) {
|
||||
this->climate_.set_model(model);
|
||||
// This public feature setter sends the current state without changing any feature bits.
|
||||
this->climate_.set_mode_bit(0, false);
|
||||
}
|
||||
|
||||
remote_base::RawTimings yaw1f_timings_(const std::array<uint8_t, 8> &command) {
|
||||
remote_base::RawTimings timings;
|
||||
append_frame(timings, command, 4500, 19980);
|
||||
timings.push_back(-40000);
|
||||
append_frame(timings, {0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0xA0}, 4500, 19980);
|
||||
timings.push_back(0);
|
||||
return timings;
|
||||
}
|
||||
|
||||
GreeClimate climate_;
|
||||
CapturingTransmitter transmitter_;
|
||||
};
|
||||
|
||||
TEST_F(GreeTransmitTest, Yaw1fMatchesCapturedOffCommandAndFollowup) {
|
||||
this->transmit_(GREE_YAW1F);
|
||||
// Captured from the flashed device on 26 September: off, 29 degrees, fan auto, swing off.
|
||||
EXPECT_EQ(this->transmitter_.data().get_data(),
|
||||
this->yaw1f_timings_({0x00, 0x0D, 0x60, 0x50, 0x00, 0x80, 0x00, 0xF0}));
|
||||
EXPECT_EQ(this->transmitter_.data().get_carrier_frequency(), 38000);
|
||||
EXPECT_EQ(this->transmitter_.send_count, 1);
|
||||
}
|
||||
|
||||
TEST_F(GreeTransmitTest, Yaw1fUpdatesModeTemperatureFanSwingAndChecksum) {
|
||||
this->transmit_(GREE_YAW1F);
|
||||
this->climate_.mode = climate::CLIMATE_MODE_COOL;
|
||||
this->climate_.target_temperature = 26;
|
||||
this->climate_.fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
this->climate_.swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||
this->transmit_(GREE_YAW1F);
|
||||
EXPECT_EQ(this->transmitter_.data().get_data(),
|
||||
this->yaw1f_timings_({0x79, 0x0A, 0x60, 0x50, 0x11, 0x80, 0x00, 0x60}));
|
||||
EXPECT_EQ(this->transmitter_.send_count, 2);
|
||||
}
|
||||
|
||||
TEST_F(GreeTransmitTest, ExistingModelsKeepTheirBytesAndSingleMessageFraming) {
|
||||
struct ModelVector {
|
||||
Model model;
|
||||
std::array<uint8_t, 8> bytes;
|
||||
};
|
||||
const ModelVector vectors[] = {
|
||||
{GREE_GENERIC, {0x00, 0x0D, 0x00, 0x00, 0x00, 0x20, 0x00, 0x90}},
|
||||
// Byte 2 carries the display light bit, which is on until a switch turns it off.
|
||||
{GREE_YAN, {0x00, 0x0D, 0x20, 0x50, 0x00, 0x20, 0x00, 0x90}},
|
||||
{GREE_YAA, {0x00, 0x0D, 0x20, 0x50, 0x00, 0x20, 0x20, 0xB0}},
|
||||
{GREE_YAC, {0x00, 0x0D, 0x20, 0x50, 0x00, 0x20, 0x20, 0xB0}},
|
||||
{GREE_YAC1FB9, {0x00, 0x0D, 0x20, 0x50, 0x00, 0x20, 0x20, 0xB0}},
|
||||
{GREE_YX1FF, {0x00, 0x0D, 0x20, 0x50, 0x00, 0x20, 0x00, 0x90}},
|
||||
{GREE_YAG, {0x00, 0x0D, 0x60, 0x50, 0x00, 0x40, 0x00, 0xB0}},
|
||||
};
|
||||
for (const auto &vector : vectors) {
|
||||
SCOPED_TRACE(static_cast<int>(vector.model));
|
||||
this->transmit_(vector.model);
|
||||
remote_base::RawTimings expected;
|
||||
const bool yac1fb9 = vector.model == GREE_YAC1FB9;
|
||||
append_frame(expected, vector.bytes, yac1fb9 ? 4500 : 4000, yac1fb9 ? 19980 : 19000);
|
||||
expected.push_back(0);
|
||||
EXPECT_EQ(this->transmitter_.data().get_data(), expected);
|
||||
EXPECT_EQ(this->transmitter_.data().get_carrier_frequency(), 38000);
|
||||
}
|
||||
EXPECT_EQ(this->transmitter_.send_count, 7);
|
||||
}
|
||||
|
||||
} // namespace esphome::gree::testing
|
||||
@@ -1,8 +0,0 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE YAW1F
|
||||
model: yaw1f
|
||||
transmitter_id: xmitr
|
||||
@@ -1,8 +0,0 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE YAW1F
|
||||
model: yaw1f
|
||||
transmitter_id: xmitr
|
||||
@@ -1,11 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The send action tests read the response body, which the real to_code enables for
|
||||
# configs that capture it.
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -72,6 +72,23 @@ button:
|
||||
id(ota_update).set_source_url(std::string("http://my.ha.net:8123/local/esphome/manifest.json"));
|
||||
ESP_LOGI("test", "Manifest is now %s", id(ota_update).get_source_url());
|
||||
- component.update: ota_update
|
||||
# The OTA component's public setters and getters as lambdas use them; every shape must compile.
|
||||
- platform: template
|
||||
name: Flash from lambda
|
||||
on_press:
|
||||
then:
|
||||
- lambda: |-
|
||||
std::string url = "http://my.ha.net:8123/local/esphome/";
|
||||
url += "firmware.bin";
|
||||
id(http_request_ota).set_md5_url("http://my.ha.net:8123/local/esphome/firmware.md5");
|
||||
id(http_request_ota).set_md5(std::string("0123456789abcdef0123456789abcdef"));
|
||||
id(http_request_ota).set_username("user");
|
||||
id(http_request_ota).set_password(std::string("pass"));
|
||||
id(http_request_ota).set_url(url);
|
||||
id(http_request_ota).set_url(url.c_str());
|
||||
ESP_LOGI("test", "MD5 %s / %s", id(http_request_ota).md5_expected().c_str(),
|
||||
id(http_request_ota).md5_computed().c_str());
|
||||
id(http_request_ota).flash();
|
||||
|
||||
update:
|
||||
- platform: http_request
|
||||
|
||||
@@ -87,7 +87,6 @@ esphome:
|
||||
http_request:
|
||||
useragent: esphome/tagreader
|
||||
timeout: 10s
|
||||
max_concurrent_requests: 2
|
||||
verify_ssl: ${verify_ssl}
|
||||
|
||||
output:
|
||||
|
||||
@@ -1,612 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::http_request::testing {
|
||||
|
||||
// A response that stays pending for a number of polls, then serves its body in fixed chunks
|
||||
class FakeContainer : public HttpContainer {
|
||||
public:
|
||||
HttpRequestProgress poll() override {
|
||||
this->polls++;
|
||||
if (this->poll_delay_ms > 0)
|
||||
delay(this->poll_delay_ms);
|
||||
if (this->pending_polls > 0) {
|
||||
this->pending_polls--;
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
return this->result;
|
||||
}
|
||||
int read(uint8_t *buf, size_t max_len) override { return this->read_available(buf, max_len); }
|
||||
int read_available(uint8_t *buf, size_t max_len) override {
|
||||
if (this->stall)
|
||||
return 0;
|
||||
if (this->bytes_read_ >= this->fail_at)
|
||||
return -1;
|
||||
size_t len = std::min({max_len, this->chunk, this->body.size() - this->bytes_read_});
|
||||
std::memcpy(buf, this->body.data() + this->bytes_read_, len);
|
||||
this->bytes_read_ += len;
|
||||
return static_cast<int>(len);
|
||||
}
|
||||
void end() override { this->ends++; }
|
||||
|
||||
int pending_polls{0};
|
||||
// Time each poll() blocks, like the first connect on ESP-IDF
|
||||
uint32_t poll_delay_ms{0};
|
||||
HttpRequestProgress result{HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE};
|
||||
std::string body;
|
||||
size_t chunk{512};
|
||||
bool stall{false};
|
||||
// Reads fail once this many bytes have been read
|
||||
size_t fail_at{SIZE_MAX};
|
||||
int polls{0};
|
||||
int ends{0};
|
||||
};
|
||||
|
||||
class FakeHttpRequest : public HttpRequestComponent {
|
||||
public:
|
||||
std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) override {
|
||||
this->begins++;
|
||||
if (this->refuse)
|
||||
return nullptr;
|
||||
auto container = std::make_shared<FakeContainer>();
|
||||
container->status_code = 200;
|
||||
container->pending_polls = this->pending_polls;
|
||||
container->poll_delay_ms = this->poll_delay_ms;
|
||||
container->body = this->response_body;
|
||||
container->content_length = this->response_body.size();
|
||||
this->containers.push_back(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
int begins{0};
|
||||
bool refuse{false};
|
||||
int pending_polls{2};
|
||||
uint32_t poll_delay_ms{0};
|
||||
std::string response_body;
|
||||
std::vector<std::shared_ptr<FakeContainer>> containers;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) override {
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// The URL lambda must be stateless, so the stop it makes is set up through this
|
||||
struct UrlStop {
|
||||
Automation<> *target{nullptr};
|
||||
int calls{0};
|
||||
int stop_at_call{0};
|
||||
};
|
||||
|
||||
UrlStop &url_stop() {
|
||||
static UrlStop state;
|
||||
return state;
|
||||
}
|
||||
|
||||
const char *url_that_can_stop() {
|
||||
UrlStop &state = url_stop();
|
||||
if (++state.calls == state.stop_at_call)
|
||||
state.target->stop();
|
||||
return "http://example.invalid/";
|
||||
}
|
||||
|
||||
class CountAction : public Action<> {
|
||||
public:
|
||||
explicit CountAction(int *count) : count_(count) {}
|
||||
|
||||
protected:
|
||||
void play() override { (*this->count_)++; }
|
||||
int *count_;
|
||||
};
|
||||
|
||||
class HttpRequestSendActionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->action_.set_url([]() -> std::string { return "http://example.invalid/"; });
|
||||
this->action_.set_method([]() -> const char * { return "GET"; });
|
||||
this->action_.set_max_response_buffer_size(1024);
|
||||
this->automation_.add_actions({&this->action_, &this->next_});
|
||||
url_stop() = UrlStop{&this->automation_};
|
||||
}
|
||||
|
||||
// One pass of the main loop
|
||||
void loop_() {
|
||||
LoopBlockingGuard guard{nullptr, nullptr, millis()};
|
||||
this->http_.loop();
|
||||
}
|
||||
|
||||
void run_until_idle_() {
|
||||
for (int i = 0; i != 100 && this->automation_.is_running(); i++)
|
||||
this->loop_();
|
||||
}
|
||||
|
||||
void capture_response_() {
|
||||
this->action_.set_capture_response([]() { return true; });
|
||||
this->with_body_.add_action(&this->store_body_);
|
||||
}
|
||||
|
||||
FakeHttpRequest http_;
|
||||
HttpRequestSendAction<> action_{&this->http_};
|
||||
int next_count_{0};
|
||||
CountAction next_{&this->next_count_};
|
||||
Trigger<> trigger_;
|
||||
Automation<> automation_{&this->trigger_};
|
||||
|
||||
int responses_{0};
|
||||
int errors_{0};
|
||||
std::string body_;
|
||||
Automation<std::shared_ptr<HttpContainer>> on_response_{this->action_.get_success_trigger()};
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> count_response_{
|
||||
[this](const std::shared_ptr<HttpContainer> &) { this->responses_++; }};
|
||||
Automation<> on_error_{this->action_.get_error_trigger()};
|
||||
CountAction count_error_{&this->errors_};
|
||||
Automation<std::shared_ptr<HttpContainer>, std::string &> with_body_{
|
||||
this->action_.get_success_trigger_with_response()};
|
||||
LambdaAction<std::shared_ptr<HttpContainer>, std::string &> store_body_{
|
||||
[this](const std::shared_ptr<HttpContainer> &, std::string &body) {
|
||||
this->responses_++;
|
||||
this->body_ = body;
|
||||
}};
|
||||
|
||||
public:
|
||||
HttpRequestSendActionTest() {
|
||||
this->on_response_.add_action(&this->count_response_);
|
||||
this->on_error_.add_action(&this->count_error_);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, ChainWaitsForTheResponse) {
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, RequestThatCannotStartFiresOnErrorAtOnce) {
|
||||
this->http_.refuse = true;
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, FailedRequestFiresOnError) {
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->result = HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, SecondTriggerWaitsForTheFirstRequest) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
|
||||
// The first finishes, and the second starts on the next pass
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, TriggersBeyondTheQueueAreDropped) {
|
||||
for (int i = 0; i != 7; i++)
|
||||
this->trigger_.trigger();
|
||||
// One running, four waiting, two dropped with on_error
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->errors_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->http_.begins, 5);
|
||||
EXPECT_EQ(this->responses_, 5);
|
||||
EXPECT_EQ(this->next_count_, 7);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, WaitingRequestThatCannotStartFiresOnError) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->http_.refuse = true;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StopEndsRequestsWithoutTriggers) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->automation_.stop();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
|
||||
// The action is usable again after a stop
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StopFromOnResponseDropsTheRest) {
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> stop{
|
||||
[this](const std::shared_ptr<HttpContainer> &) { this->automation_.stop(); }};
|
||||
this->on_response_.add_action(&stop);
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
// A script in mode restart that executes itself from on_response
|
||||
TEST_F(HttpRequestSendActionTest, RestartFromOnResponseContinuesOnlyTheNewRun) {
|
||||
int restarts = 0;
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> restart{[this, &restarts](const std::shared_ptr<HttpContainer> &) {
|
||||
if (restarts++ == 0) {
|
||||
this->automation_.stop();
|
||||
this->trigger_.trigger();
|
||||
}
|
||||
}};
|
||||
this->on_response_.add_action(&restart);
|
||||
this->trigger_.trigger();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, RestartFromOnErrorContinuesOnlyTheNewRun) {
|
||||
LambdaAction<> restart{[this]() {
|
||||
if (this->http_.refuse) {
|
||||
this->http_.refuse = false;
|
||||
this->automation_.stop();
|
||||
this->trigger_.trigger();
|
||||
}
|
||||
}};
|
||||
this->on_error_.add_action(&restart);
|
||||
this->http_.refuse = true;
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, TriggerFromOnResponseStartsAtOnce) {
|
||||
int retriggers = 0;
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> again{[this, &retriggers](const std::shared_ptr<HttpContainer> &) {
|
||||
if (retriggers++ == 0)
|
||||
this->trigger_.trigger();
|
||||
}};
|
||||
this->on_response_.add_action(&again);
|
||||
this->trigger_.trigger();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, CapturedBodyIsReadAcrossPasses) {
|
||||
this->capture_response_();
|
||||
this->http_.pending_polls = 0;
|
||||
this->http_.response_body = std::string(900, 'a');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->chunk = 100;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->body_, std::string(900, 'a'));
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, CapturedBodyStopsAtTheBufferSize) {
|
||||
this->capture_response_();
|
||||
this->action_.set_max_response_buffer_size(300);
|
||||
this->http_.response_body = std::string(1000, 'b');
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->body_, std::string(300, 'b'));
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StalledBodyEndsAtTheTimeout) {
|
||||
this->capture_response_();
|
||||
this->action_.set_timeout([]() -> uint32_t { return 20; });
|
||||
this->http_.response_body = std::string(1000, 'c');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->stall = true;
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
delay(30);
|
||||
this->loop_();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_TRUE(this->body_.empty());
|
||||
}
|
||||
|
||||
// A url lambda that stops the action while the request is being started
|
||||
TEST_F(HttpRequestSendActionTest, StopFromLambdaWhileStartingEndsTheRequest) {
|
||||
this->action_.set_url([]() -> std::string { return url_that_can_stop(); });
|
||||
url_stop().stop_at_call = 1;
|
||||
this->trigger_.trigger();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
ASSERT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StopFromLambdaOfAWaitingRequestEndsIt) {
|
||||
this->action_.set_url([]() -> std::string { return url_that_can_stop(); });
|
||||
url_stop().stop_at_call = 2;
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
ASSERT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->http_.containers[1]->polls, 0);
|
||||
EXPECT_EQ(this->http_.containers[1]->ends, 1);
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
|
||||
// The slot counts survived the stop, so a new trigger starts at once
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 3);
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, ReadErrorKeepsTheBodyReadSoFar) {
|
||||
this->capture_response_();
|
||||
this->http_.response_body = std::string(800, 'd');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->chunk = 100;
|
||||
this->http_.containers[0]->fail_at = 300;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->body_, std::string(300, 'd'));
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, BufferThatCannotBeAllocatedGivesAnEmptyBody) {
|
||||
this->capture_response_();
|
||||
this->action_.set_max_response_buffer_size(SIZE_MAX);
|
||||
this->http_.response_body = "unread";
|
||||
this->body_ = "not set";
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_TRUE(this->body_.empty());
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
// A forced reboot calls only on_shutdown(), which finishes the requests in progress
|
||||
TEST_F(HttpRequestSendActionTest, ShutdownFinishesTheRequestInProgress) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
this->http_.on_shutdown();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
// An on_shutdown automation that runs after the component's on_shutdown()
|
||||
TEST_F(HttpRequestSendActionTest, RequestAfterShutdownFinishesAtOnce) {
|
||||
this->http_.on_shutdown();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, BodyTimeoutStartsAfterABlockingPoll) {
|
||||
this->capture_response_();
|
||||
this->action_.set_timeout([]() -> uint32_t { return 20; });
|
||||
this->http_.pending_polls = 0;
|
||||
this->http_.poll_delay_ms = 30;
|
||||
this->http_.response_body = std::string(100, 'e');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->stall = true;
|
||||
this->loop_();
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
this->http_.containers[0]->stall = false;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->body_, std::string(100, 'e'));
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, DurationCoversTheWholeBody) {
|
||||
this->capture_response_();
|
||||
this->http_.pending_polls = 0;
|
||||
this->http_.response_body = std::string(100, 'f');
|
||||
this->trigger_.trigger();
|
||||
auto container = this->http_.containers[0];
|
||||
container->duration_ms = 1;
|
||||
container->stall = true;
|
||||
this->loop_();
|
||||
delay(30);
|
||||
container->stall = false;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_GE(container->duration_ms, 30u);
|
||||
}
|
||||
|
||||
// The limit is per http_request component, so that one client is held at a time
|
||||
TEST_F(HttpRequestSendActionTest, TwoActionsTakeTurns) {
|
||||
HttpRequestSendAction<> other{&this->http_};
|
||||
other.set_url([]() -> std::string { return "http://example.invalid/other"; });
|
||||
other.set_method([]() -> const char * { return "GET"; });
|
||||
int other_next = 0;
|
||||
CountAction count_other{&other_next};
|
||||
Trigger<> other_trigger;
|
||||
Automation<> other_automation{&other_trigger};
|
||||
other_automation.add_actions({&other, &count_other});
|
||||
|
||||
this->trigger_.trigger();
|
||||
other_trigger.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_EQ(other_next, 0);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
|
||||
for (int i = 0; i != 10 && other_automation.is_running(); i++)
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(other_next, 1);
|
||||
}
|
||||
|
||||
// Stopping one action leaves the requests of another alone
|
||||
TEST_F(HttpRequestSendActionTest, StopEndsOnlyThatActionsRequests) {
|
||||
HttpRequestSendAction<> other{&this->http_};
|
||||
other.set_url([]() -> std::string { return "http://example.invalid/other"; });
|
||||
other.set_method([]() -> const char * { return "GET"; });
|
||||
int other_next = 0;
|
||||
CountAction count_other{&other_next};
|
||||
Trigger<> other_trigger;
|
||||
Automation<> other_automation{&other_trigger};
|
||||
other_automation.add_actions({&other, &count_other});
|
||||
|
||||
other_trigger.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->automation_.stop();
|
||||
for (int i = 0; i != 10 && other_automation.is_running(); i++)
|
||||
this->loop_();
|
||||
EXPECT_EQ(other_next, 1);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
}
|
||||
|
||||
// A safe shutdown drops the requests already made, so they do not hold up the reboot,
|
||||
// and still sends the ones that on_shutdown automations make
|
||||
TEST_F(HttpRequestSendActionTest, SafeShutdownSendsOnlyTheShutdownRequests) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->http_.on_safe_shutdown();
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
// Stopped like script.stop, so a mode single script could run it again from on_shutdown
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
this->http_.on_shutdown();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, MaxConcurrentRequestsRunTogether) {
|
||||
this->http_.set_max_concurrent_requests(2);
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
|
||||
// Both finish on the third pass; the third request takes a free slot on the next
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 3);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 3);
|
||||
EXPECT_EQ(this->next_count_, 3);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, MaxConcurrentRequestsKeepsTheQueueLimit) {
|
||||
this->http_.set_max_concurrent_requests(2);
|
||||
for (int i = 0; i != 8; i++)
|
||||
this->trigger_.trigger();
|
||||
// Two running, four waiting, two dropped
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->errors_, 2);
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 6);
|
||||
EXPECT_EQ(this->next_count_, 8);
|
||||
}
|
||||
|
||||
} // namespace esphome::http_request::testing
|
||||
@@ -1,11 +0,0 @@
|
||||
modem:
|
||||
rx_pin: GPIO26
|
||||
tx_pin: GPIO27
|
||||
model: SIM7600
|
||||
apn: orange
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
ssid: WISP AP
|
||||
password: wisp12345678
|
||||
ap_mode: wisp
|
||||
@@ -16,7 +16,6 @@ modem:
|
||||
init_at:
|
||||
- ATI
|
||||
reboot_timeout: 5min
|
||||
update_interval: 30s
|
||||
on_connect:
|
||||
logger.log: "modem connected"
|
||||
on_disconnect:
|
||||
@@ -28,15 +27,6 @@ text_sensor:
|
||||
- platform: modem
|
||||
urc:
|
||||
name: Modem URC
|
||||
network_type:
|
||||
name: Network type
|
||||
|
||||
sensor:
|
||||
- platform: modem
|
||||
rssi:
|
||||
name: RSSI
|
||||
ber:
|
||||
name: BER
|
||||
|
||||
packages:
|
||||
modem: !include common.yaml
|
||||
@@ -12,10 +12,8 @@
|
||||
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/online_image/online_image.h"
|
||||
#include "esphome/components/runtime_image/png_decoder.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "pngle.h"
|
||||
|
||||
namespace esphome::online_image::testing {
|
||||
|
||||
@@ -137,153 +135,6 @@ static constexpr std::array<uint8_t, 69> PNG_1X1{
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9,
|
||||
0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
|
||||
|
||||
// 800x600 RGB PNG of a single color, 2312 bytes: the decoder needs several calls to draw it.
|
||||
// The same bytes are in tests/components/runtime_image/test_png_time_limit.cpp.
|
||||
static const uint8_t PNG_SOLID[] = {
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
|
||||
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
|
||||
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
|
||||
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
};
|
||||
|
||||
// Draws that take a millisecond every fourth row, so decoding PNG_SOLID always outlasts the time limit of one call
|
||||
class SlowDraws : public runtime_image::PngDecoder, public runtime_image::RuntimeImage {
|
||||
public:
|
||||
static void install(runtime_image::RuntimeImage &image) {
|
||||
auto *decoder = static_cast<runtime_image::PngDecoder *>((image.*&SlowDraws::decoder_).get());
|
||||
pngle_set_draw_callback(decoder->*&SlowDraws::pngle_, draw);
|
||||
}
|
||||
|
||||
private:
|
||||
static void draw(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
|
||||
if (x == 0 && y % 4 == 0) {
|
||||
const uint32_t until = millis() + 2;
|
||||
while (millis() < until) {
|
||||
}
|
||||
}
|
||||
static_cast<runtime_image::PngDecoder *>(pngle_get_user_data(pngle))
|
||||
->draw(x, y, w, h, Color(rgba[0], rgba[1], rgba[2], rgba[3]));
|
||||
}
|
||||
};
|
||||
|
||||
// 8x8 progressive JPEG, 521 bytes
|
||||
static const uint8_t JPEG_PROGRESSIVE[] = {
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||
@@ -607,30 +458,6 @@ TEST_F(OnlineImageDownloadEnd, JpegBufferAllocationFailureReportsError) {
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// The whole PNG arrives in one read, but the decoder stops after a short time per call: the rest is decoded on
|
||||
// later passes, not reported as data the decoder could not use
|
||||
TEST_F(OnlineImageDownloadEnd, PngDecodedOverSeveralPassesAfterTheDownloadEnds) {
|
||||
for (bool chunked : {false, true}) {
|
||||
this->finished_ = 0;
|
||||
this->errors_ = 0;
|
||||
auto image = this->make_image_(runtime_image::PNG, 4096);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID)),
|
||||
sizeof(PNG_SOLID), chunked);
|
||||
this->start_(*image, container);
|
||||
SlowDraws::install(*image);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++)
|
||||
image->loop();
|
||||
|
||||
EXPECT_EQ(this->finished_, 1) << (chunked ? "chunked" : "with Content-Length");
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_GT(passes, 2);
|
||||
EXPECT_EQ(image->get_width(), 800);
|
||||
EXPECT_EQ(image->get_pixel(400, 300).b, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// The JPEG decoded with all of it in memory at once, to compare the downloaded images with
|
||||
static std::vector<uint8_t> reference_jpeg_pixels() {
|
||||
runtime_image::RuntimeImage ref(runtime_image::JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code_testing(config: object) -> None:
|
||||
cg.add_define("USE_PACKET_INTERFACE_BASIC_SESSION")
|
||||
cg.add_define("USE_PACKET_INTERFACE_HDLC")
|
||||
cg.add_define("USE_PACKET_INTERFACE_XXTEA")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -1,252 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "esphome/components/packet_interface/basic_session.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::packet_interface::session_testing {
|
||||
|
||||
// Copies a packet into a vector, for comparing in tests
|
||||
static std::vector<uint8_t> to_vector(const esphome::packet_interface::PacketBuffer &data) {
|
||||
std::vector<uint8_t> result(data.size());
|
||||
data.copy_to(result.data(), result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
using Packet = std::vector<uint8_t>;
|
||||
|
||||
// A link layer that records what is sent, so a test can deliver it to the peer
|
||||
class MockLink : public PacketInterface {
|
||||
public:
|
||||
explicit MockLink(size_t max_packet_size = 64) : PacketInterface(max_packet_size) {}
|
||||
bool send_to_interface(const PacketBuffer &data, const PacketMetaData &) override {
|
||||
if (!this->accept)
|
||||
return false;
|
||||
this->sent.push_back(to_vector(data));
|
||||
return true;
|
||||
}
|
||||
void inject(const Packet &packet) { this->on_receive_from_interface_(PacketBuffer(packet), {}); }
|
||||
// Act as a reliability layer that resets and discards data
|
||||
void set_reliable() { this->capabilities_ = CAPABILITY_RELIABILITY; }
|
||||
void report_data_lost() { this->session_callback_.call(false); }
|
||||
|
||||
std::vector<Packet> sent;
|
||||
bool accept{true};
|
||||
};
|
||||
|
||||
class TestSession : public BasicSession {
|
||||
public:
|
||||
using BasicSession::BasicSession;
|
||||
using BasicSession::on_interval_;
|
||||
unsigned get_not_understood_count() const { return this->not_understood_count_; }
|
||||
};
|
||||
|
||||
struct Peer {
|
||||
Peer() {
|
||||
this->session = std::make_unique<TestSession>(&this->link);
|
||||
this->session->set_keep_alive(1000);
|
||||
this->session->set_session_timeout(3000);
|
||||
this->session->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->received.push_back(to_vector(data)); });
|
||||
this->session->add_on_session_state_callback([this](bool connected) { this->states.push_back(connected); });
|
||||
this->session->setup();
|
||||
}
|
||||
bool send(const Packet &data) { return this->session->send_to_interface(PacketBuffer(data), {}); }
|
||||
|
||||
MockLink link;
|
||||
std::unique_ptr<TestSession> session;
|
||||
std::vector<Packet> received;
|
||||
std::vector<bool> states;
|
||||
};
|
||||
|
||||
static bool is_keep_alive(const Packet &packet) { return !packet.empty() && packet[0] == 0x01; }
|
||||
|
||||
// Move packets between the peers until nothing more is sent
|
||||
static void pump(Peer &a, Peer &b) {
|
||||
for (int i = 0; i != 20; i++) {
|
||||
if (a.link.sent.empty() && b.link.sent.empty())
|
||||
return;
|
||||
auto from_a = std::move(a.link.sent);
|
||||
auto from_b = std::move(b.link.sent);
|
||||
a.link.sent.clear();
|
||||
b.link.sent.clear();
|
||||
for (auto &packet : from_a)
|
||||
b.link.inject(packet);
|
||||
for (auto &packet : from_b)
|
||||
a.link.inject(packet);
|
||||
}
|
||||
FAIL() << "Peers did not go quiet";
|
||||
}
|
||||
|
||||
// Each test replaces App with a fresh one; the keep-alive interval needs its scheduler
|
||||
class BasicSessionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
App.~Application();
|
||||
new (&App) Application();
|
||||
static char name[] = "session-tests";
|
||||
static char friendly_name[] = "";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
|
||||
}
|
||||
void TearDown() override {
|
||||
App.~Application();
|
||||
new (&App) Application(); // later tests and the test listener use App
|
||||
}
|
||||
|
||||
static void set_time(uint32_t now) { LoopBlockingGuard guard{nullptr, nullptr, now}; }
|
||||
};
|
||||
|
||||
TEST_F(BasicSessionTest, MaxPacketSizeAllowsForHeader) {
|
||||
Peer a;
|
||||
EXPECT_EQ(a.session->get_max_packet_size(), 64 - BasicSession::HEADER_SIZE);
|
||||
EXPECT_EQ(a.link.top(), a.session.get());
|
||||
EXPECT_EQ(a.session->get_capabilities(), CAPABILITY_SESSION);
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, KeepAliveSentAtSetup) {
|
||||
Peer a;
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_keep_alive(a.link.sent[0]));
|
||||
EXPECT_EQ(a.link.sent[0].size(), 5);
|
||||
EXPECT_FALSE(a.session->is_connected());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, BothConnectWithoutRepeatedAnswers) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_TRUE(b.session->is_connected());
|
||||
EXPECT_EQ(a.states, std::vector<bool>{true});
|
||||
EXPECT_EQ(b.states, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, OneSideStartsLater) {
|
||||
Peer a;
|
||||
a.link.sent.clear(); // a's first keep-alive is lost
|
||||
Peer b;
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_TRUE(b.session->is_connected());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataPassesThroughWithHeader) {
|
||||
Peer a, b;
|
||||
pump(a, b);
|
||||
ASSERT_TRUE(a.send({0x10, 0x11}));
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_EQ(a.link.sent[0], (Packet{0x00, 0x10, 0x11}));
|
||||
pump(a, b);
|
||||
EXPECT_EQ(b.received, (std::vector<Packet>{{0x10, 0x11}}));
|
||||
EXPECT_TRUE(a.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, NotUnderstoodPacketsCountedUntilOneIsUnderstood) {
|
||||
Peer a;
|
||||
for (int i = 0; i != 20; i++)
|
||||
a.link.inject({0x7F, 0x00});
|
||||
// The count stops where the mismatch is logged, so it is logged once
|
||||
EXPECT_EQ(a.session->get_not_understood_count(), 10);
|
||||
a.link.inject({0x00, 0x55});
|
||||
EXPECT_EQ(a.session->get_not_understood_count(), 0);
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataMarksPeerConnected) {
|
||||
Peer a;
|
||||
a.link.inject({0x00, 0x42});
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_EQ(a.received, (std::vector<Packet>{{0x42}}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataBeforeFirstKeepAliveIsNotARestart) {
|
||||
Peer a, b;
|
||||
a.link.sent.clear();
|
||||
b.link.sent.clear(); // b's first keep-alive is lost
|
||||
b.send({0x42});
|
||||
pump(a, b);
|
||||
b.session->on_interval_();
|
||||
pump(a, b);
|
||||
EXPECT_EQ(a.states, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, SendFailureIsReturned) {
|
||||
Peer a;
|
||||
a.link.accept = false;
|
||||
EXPECT_FALSE(a.send({0x01}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, IntervalSendsKeepAlive) {
|
||||
Peer a;
|
||||
a.link.sent.clear();
|
||||
a.session->on_interval_();
|
||||
ASSERT_EQ(a.link.sent.size(), 1);
|
||||
EXPECT_TRUE(is_keep_alive(a.link.sent[0]));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, PeerLostAfterTimeout) {
|
||||
Peer a, b;
|
||||
set_time(1000);
|
||||
pump(a, b);
|
||||
set_time(4000);
|
||||
a.session->on_interval_();
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
set_time(4001);
|
||||
a.session->on_interval_();
|
||||
EXPECT_FALSE(a.session->is_connected());
|
||||
EXPECT_EQ(a.states, (std::vector<bool>{true, false}));
|
||||
// Heard again
|
||||
b.session->on_interval_();
|
||||
pump(a, b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_EQ(a.states, (std::vector<bool>{true, false, true}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, PeerRestartIsReported) {
|
||||
Peer a;
|
||||
auto b = std::make_unique<Peer>();
|
||||
pump(a, *b);
|
||||
b = std::make_unique<Peer>();
|
||||
pump(a, *b);
|
||||
EXPECT_TRUE(a.session->is_connected());
|
||||
EXPECT_EQ(a.states, (std::vector<bool>{true, false, true}));
|
||||
EXPECT_EQ(b->states, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, MalformedPacketsDropped) {
|
||||
Peer a;
|
||||
a.link.inject({});
|
||||
a.link.inject({0x02, 0x00});
|
||||
a.link.inject({0x01, 0x00, 0x00});
|
||||
EXPECT_FALSE(a.session->is_connected());
|
||||
EXPECT_TRUE(a.received.empty());
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, DataLostBelowReportedAsSessionLost) {
|
||||
MockLink link;
|
||||
link.set_reliable();
|
||||
TestSession session(&link);
|
||||
std::vector<bool> states;
|
||||
session.add_on_session_state_callback([&states](bool connected) { states.push_back(connected); });
|
||||
session.setup();
|
||||
// Not yet connected, so nothing to report
|
||||
link.report_data_lost();
|
||||
EXPECT_TRUE(states.empty());
|
||||
link.inject({0x00, 0x55});
|
||||
link.report_data_lost();
|
||||
EXPECT_FALSE(session.is_connected());
|
||||
// Connected again when the peer is next heard
|
||||
link.inject({0x00, 0x56});
|
||||
EXPECT_EQ(states, (std::vector<bool>{true, false, true}));
|
||||
}
|
||||
|
||||
TEST_F(BasicSessionTest, LinkTooSmallForKeepAliveFailsSetup) {
|
||||
MockLink link(4);
|
||||
TestSession session(&link);
|
||||
session.setup();
|
||||
EXPECT_TRUE(session.is_failed());
|
||||
EXPECT_TRUE(link.sent.empty());
|
||||
Packet packet{1};
|
||||
EXPECT_FALSE(session.send_to_interface(PacketBuffer(packet), {}));
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface::session_testing
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user