Compare commits

..
121 changed files with 507 additions and 6871 deletions

No files matched your search

-2
View File
@@ -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")
+2 -2
View File
@@ -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;
}
-3
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -1 +0,0 @@
DOMAIN = "es7243"
-52
View File
@@ -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]))
-89
View File
@@ -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
-36
View File
@@ -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
+1 -3
View File
@@ -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
+1 -3
View File
@@ -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")
+47 -56
View File
@@ -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;
+2 -7
View File
@@ -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:
+3 -10
View File
@@ -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
+47 -157
View File
@@ -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,
+1
View File
@@ -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 -1
View File
@@ -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,
+1 -17
View File
@@ -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)
+1 -91
View File
@@ -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) {
+1 -38
View File
@@ -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
-49
View File
@@ -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)
+1 -33
View File
@@ -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)
+63 -65
View File
@@ -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]))
+8 -1
View File
@@ -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 = "";
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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"
+12 -51
View File
@@ -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
+2 -7
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+2 -5
View File
@@ -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();
+14 -34
View File
@@ -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
+3 -4
View File
@@ -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 -8
View File
@@ -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) {
-7
View File
@@ -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
+1 -3
View File
@@ -296,9 +296,7 @@ def _external_manifests() -> list[dict[str, Any]]:
found[component_dir] = source
break
else:
# Built in means anywhere in the package: the esphome domain itself
# is esphome/core, not under esphome/components
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH.parent):
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH):
_LOGGER.warning(
"SBOM: component '%s' in %s matches no external component "
"source and is listed as part of ESPHome",
+30 -88
View File
@@ -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
)
-43
View File
@@ -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
View File
@@ -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
-6
View File
@@ -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"]
-57
View File
@@ -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
-15
View File
@@ -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
-8
View File
@@ -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
-208
View File
@@ -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
-11
View File
@@ -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
@@ -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 121 files, more files were not shown because too many files have changed in this diff. Show more