Merge branch 'dev' into camera-platform

This commit is contained in:
J. Nick Koston 2025-07-03 13:37:54 -05:00 committed by GitHub
commit 756ece9ff3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1862 additions and 99 deletions

View File

@ -442,6 +442,7 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx127x/* @swoboda1337
esphome/components/syslog/* @clydebarrow esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan

View File

@ -95,19 +95,6 @@ APIConnection::~APIConnection() {
#endif #endif
} }
#ifdef HAS_PROTO_MESSAGE_DUMP
void APIConnection::log_batch_item_(const DeferredBatch::BatchItem &item) {
// Set log-only mode
this->flags_.log_only_mode = true;
// Call the creator - it will create the message and log it via encode_message_to_buffer
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
// Clear log-only mode
this->flags_.log_only_mode = false;
}
#endif
void APIConnection::loop() { void APIConnection::loop() {
if (this->flags_.next_close) { if (this->flags_.next_close) {
// requested a disconnect // requested a disconnect
@ -159,15 +146,25 @@ void APIConnection::loop() {
} }
} }
// Process deferred batch if scheduled // Process deferred batch if scheduled and timer has expired
if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
this->process_batch_(); this->process_batch_();
} }
if (!this->list_entities_iterator_.completed()) { if (!this->list_entities_iterator_.completed()) {
this->list_entities_iterator_.advance(); this->process_iterator_batch_(this->list_entities_iterator_);
} else if (!this->initial_state_iterator_.completed()) { } else if (!this->initial_state_iterator_.completed()) {
this->initial_state_iterator_.advance(); this->process_iterator_batch_(this->initial_state_iterator_);
// If we've completed initial states, process any remaining and clear the flag
if (this->initial_state_iterator_.completed()) {
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
}
} }
if (this->flags_.sent_ping) { if (this->flags_.sent_ping) {
@ -305,8 +302,8 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
return this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_state, return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state,
BinarySensorStateResponse::MESSAGE_TYPE); BinarySensorStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -333,7 +330,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
#ifdef USE_COVER #ifdef USE_COVER
bool APIConnection::send_cover_state(cover::Cover *cover) { bool APIConnection::send_cover_state(cover::Cover *cover) {
return this->schedule_message_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -394,7 +391,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
#ifdef USE_FAN #ifdef USE_FAN
bool APIConnection::send_fan_state(fan::Fan *fan) { bool APIConnection::send_fan_state(fan::Fan *fan) {
return this->schedule_message_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -453,7 +450,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool APIConnection::send_light_state(light::LightState *light) { bool APIConnection::send_light_state(light::LightState *light) {
return this->schedule_message_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -545,7 +542,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
return this->schedule_message_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -577,7 +574,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool APIConnection::send_switch_state(switch_::Switch *a_switch) { bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
return this->schedule_message_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -614,8 +611,8 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) { bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
return this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_state, return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state,
TextSensorStateResponse::MESSAGE_TYPE); TextSensorStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -642,7 +639,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool APIConnection::send_climate_state(climate::Climate *climate) { bool APIConnection::send_climate_state(climate::Climate *climate) {
return this->schedule_message_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE); return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -742,7 +739,7 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool APIConnection::send_number_state(number::Number *number) { bool APIConnection::send_number_state(number::Number *number) {
return this->schedule_message_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -782,7 +779,7 @@ void APIConnection::number_command(const NumberCommandRequest &msg) {
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool APIConnection::send_date_state(datetime::DateEntity *date) { bool APIConnection::send_date_state(datetime::DateEntity *date) {
return this->schedule_message_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE); return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -816,7 +813,7 @@ void APIConnection::date_command(const DateCommandRequest &msg) {
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool APIConnection::send_time_state(datetime::TimeEntity *time) { bool APIConnection::send_time_state(datetime::TimeEntity *time) {
return this->schedule_message_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE); return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -850,8 +847,8 @@ void APIConnection::time_command(const TimeCommandRequest &msg) {
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
return this->schedule_message_(datetime, &APIConnection::try_send_datetime_state, return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state,
DateTimeStateResponse::MESSAGE_TYPE); DateTimeStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -886,7 +883,7 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
#ifdef USE_TEXT #ifdef USE_TEXT
bool APIConnection::send_text_state(text::Text *text) { bool APIConnection::send_text_state(text::Text *text) {
return this->schedule_message_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -924,7 +921,7 @@ void APIConnection::text_command(const TextCommandRequest &msg) {
#ifdef USE_SELECT #ifdef USE_SELECT
bool APIConnection::send_select_state(select::Select *select) { bool APIConnection::send_select_state(select::Select *select) {
return this->schedule_message_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -979,7 +976,7 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg
#ifdef USE_LOCK #ifdef USE_LOCK
bool APIConnection::send_lock_state(lock::Lock *a_lock) { bool APIConnection::send_lock_state(lock::Lock *a_lock) {
return this->schedule_message_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -1023,7 +1020,7 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
#ifdef USE_VALVE #ifdef USE_VALVE
bool APIConnection::send_valve_state(valve::Valve *valve) { bool APIConnection::send_valve_state(valve::Valve *valve) {
return this->schedule_message_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE); return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1063,8 +1060,8 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) {
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
return this->schedule_message_(media_player, &APIConnection::try_send_media_player_state, return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state,
MediaPlayerStateResponse::MESSAGE_TYPE); MediaPlayerStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1325,8 +1322,8 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state, return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state,
AlarmControlPanelStateResponse::MESSAGE_TYPE); AlarmControlPanelStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
@ -1409,7 +1406,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) { bool APIConnection::send_update_state(update::UpdateEntity *update) {
return this->schedule_message_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE); return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE);
} }
uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1756,11 +1753,16 @@ void APIConnection::process_batch_() {
if (payload_size > 0 && if (payload_size > 0 &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
this->deferred_batch_.clear(); #ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
this->log_batch_item_(item);
#endif
this->clear_batch_();
} else if (payload_size == 0) { } else if (payload_size == 0) {
// Message too large // Message too large
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type); ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
this->deferred_batch_.clear(); this->clear_batch_();
} }
return; return;
} }
@ -1869,7 +1871,7 @@ void APIConnection::process_batch_() {
this->schedule_batch_(); this->schedule_batch_();
} else { } else {
// All items processed // All items processed
this->deferred_batch_.clear(); this->clear_batch_();
} }
} }

View File

@ -18,6 +18,8 @@ namespace api {
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
static constexpr size_t MAX_INITIAL_PER_BATCH = 20;
class APIConnection : public APIServerConnection { class APIConnection : public APIServerConnection {
public: public:
@ -296,6 +298,20 @@ class APIConnection : public APIServerConnection {
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
// Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
iterator.advance();
}
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
this->process_batch_();
}
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
@ -582,7 +598,8 @@ class APIConnection : public APIServerConnection {
uint8_t service_call_subscription : 1; uint8_t service_call_subscription : 1;
uint8_t next_close : 1; uint8_t next_close : 1;
uint8_t batch_scheduled : 1; uint8_t batch_scheduled : 1;
uint8_t batch_first_message : 1; // For batch buffer allocation uint8_t batch_first_message : 1; // For batch buffer allocation
uint8_t should_try_send_immediately : 1; // True after initial states are sent
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
uint8_t log_only_mode : 1; uint8_t log_only_mode : 1;
#endif #endif
@ -609,11 +626,50 @@ class APIConnection : public APIServerConnection {
bool schedule_batch_(); bool schedule_batch_();
void process_batch_(); void process_batch_();
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;
}
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void log_batch_item_(const DeferredBatch::BatchItem &item); // Helper to log a proto message from a MessageCreator object
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) {
this->flags_.log_only_mode = true;
creator(entity, this, MAX_PACKET_SIZE, true, message_type);
this->flags_.log_only_mode = false;
}
void log_batch_item_(const DeferredBatch::BatchItem &item) {
// Use the helper to log the message
this->log_proto_message_(item.entity, item.creator, item.message_type);
}
#endif #endif
// Helper method to send a message either immediately or via batching
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) {
// Try to send immediately if:
// 1. We should try to send immediately (should_try_send_immediately = true)
// 2. Batch delay is 0 (user has opted in to immediate sending)
// 3. Buffer has space available
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, MessageCreator(creator), message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type);
}
// Helper function to schedule a deferred message with known message type // Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type); this->deferred_batch_.add_item(entity, std::move(creator), message_type);

View File

@ -5,17 +5,15 @@ namespace microphone {
void Microphone::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) { void Microphone::add_data_callback(std::function<void(const std::vector<uint8_t> &)> &&data_callback) {
std::function<void(const std::vector<uint8_t> &)> mute_handled_callback = std::function<void(const std::vector<uint8_t> &)> mute_handled_callback =
[this, data_callback](const std::vector<uint8_t> &data) { data_callback(this->silence_audio_(data)); }; [this, data_callback](const std::vector<uint8_t> &data) {
if (this->mute_state_) {
data_callback(std::vector<uint8_t>(data.size(), 0));
} else {
data_callback(data);
};
};
this->data_callbacks_.add(std::move(mute_handled_callback)); this->data_callbacks_.add(std::move(mute_handled_callback));
} }
std::vector<uint8_t> Microphone::silence_audio_(std::vector<uint8_t> data) {
if (this->mute_state_) {
std::memset((void *) data.data(), 0, data.size());
}
return data;
}
} // namespace microphone } // namespace microphone
} // namespace esphome } // namespace esphome

View File

@ -33,8 +33,6 @@ class Microphone {
audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; } audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; }
protected: protected:
std::vector<uint8_t> silence_audio_(std::vector<uint8_t> data);
State state_{STATE_STOPPED}; State state_{STATE_STOPPED};
bool mute_state_{false}; bool mute_state_{false};

View File

@ -63,6 +63,7 @@ BASE_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_URL): cv.url, cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_USERNAME): cv.string, cv.Optional(CONF_USERNAME): cv.string,
cv.Optional(CONF_PASSWORD): cv.string, cv.Optional(CONF_PASSWORD): cv.string,
cv.Exclusive(CONF_FILE, CONF_FILES): validate_yaml_filename, cv.Exclusive(CONF_FILE, CONF_FILES): validate_yaml_filename,
@ -116,6 +117,9 @@ def _process_base_package(config: dict) -> dict:
) )
files = [] files = []
if base_path := config.get(CONF_PATH):
repo_dir = repo_dir / base_path
for file in config[CONF_FILES]: for file in config[CONF_FILES]:
if isinstance(file, str): if isinstance(file, str):
files.append({CONF_PATH: file, CONF_VARS: {}}) files.append({CONF_PATH: file, CONF_VARS: {}})

View File

@ -1,19 +1,76 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor from esphome.components import binary_sensor
from esphome.const import CONF_ID import esphome.config_validation as cv
from esphome.const import (
CONF_DATA,
CONF_ID,
CONF_NAME,
CONF_STATUS,
CONF_TYPE,
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
)
import esphome.final_validate as fv
from . import ( from . import (
CONF_ENCRYPTION,
CONF_PING_PONG_ENABLE,
CONF_PROVIDER, CONF_PROVIDER,
CONF_PROVIDERS,
CONF_REMOTE_ID, CONF_REMOTE_ID,
CONF_TRANSPORT_ID, CONF_TRANSPORT_ID,
PacketTransport,
packet_transport_sensor_schema, packet_transport_sensor_schema,
provider_name_validate,
) )
CONFIG_SCHEMA = packet_transport_sensor_schema(binary_sensor.binary_sensor_schema()) STATUS_SENSOR_SCHEMA = binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(
{
cv.GenerateID(CONF_TRANSPORT_ID): cv.use_id(PacketTransport),
cv.Required(CONF_PROVIDER): provider_name_validate,
}
)
CONFIG_SCHEMA = cv.typed_schema(
{
CONF_DATA: packet_transport_sensor_schema(binary_sensor.binary_sensor_schema()),
CONF_STATUS: STATUS_SENSOR_SCHEMA,
},
key=CONF_TYPE,
default_type=CONF_DATA,
)
def _final_validate(config):
if config[CONF_TYPE] != CONF_STATUS:
# Only run this validation if a status sensor is being configured
return config
full_config = fv.full_config.get()
transport_path = full_config.get_path_for_id(config[CONF_TRANSPORT_ID])[:-1]
transport_config = full_config.get_config_for_path(transport_path)
if transport_config[CONF_PING_PONG_ENABLE] and any(
CONF_ENCRYPTION in p
for p in transport_config[CONF_PROVIDERS]
if p[CONF_NAME] == config[CONF_PROVIDER]
):
return config
raise cv.Invalid(
"Status sensor requires ping-pong to be enabled and the nominated provider to use encryption."
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = await binary_sensor.new_binary_sensor(config) var = await binary_sensor.new_binary_sensor(config)
comp = await cg.get_variable(config[CONF_TRANSPORT_ID]) comp = await cg.get_variable(config[CONF_TRANSPORT_ID])
remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) if config[CONF_TYPE] == CONF_STATUS:
cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) cg.add(comp.set_provider_status_sensor(config[CONF_PROVIDER], var))
cg.add_define("USE_STATUS_SENSOR")
else: # CONF_DATA is default
remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID))
cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var))

View File

@ -317,8 +317,37 @@ void PacketTransport::update() {
auto now = millis() / 1000; auto now = millis() / 1000;
if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) {
this->resend_ping_key_ = this->ping_pong_enable_; this->resend_ping_key_ = this->ping_pong_enable_;
ESP_LOGV(TAG, "Ping request, age %u", now - this->last_key_time_);
this->last_key_time_ = now; this->last_key_time_ = now;
} }
for (const auto &provider : this->providers_) {
uint32_t key_response_age = now - provider.second.last_key_response_time;
if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) {
#ifdef USE_STATUS_SENSOR
if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) {
ESP_LOGI(TAG, "Ping status for %s timeout at %u with age %u", provider.first.c_str(), now, key_response_age);
provider.second.status_sensor->publish_state(false);
}
#endif
#ifdef USE_SENSOR
for (auto &sensor : this->remote_sensors_[provider.first]) {
sensor.second->publish_state(NAN);
}
#endif
#ifdef USE_BINARY_SENSOR
for (auto &sensor : this->remote_binary_sensors_[provider.first]) {
sensor.second->invalidate_state();
}
#endif
} else {
#ifdef USE_STATUS_SENSOR
if (provider.second.status_sensor != nullptr && !provider.second.status_sensor->state) {
ESP_LOGI(TAG, "Ping status for %s restored at %u with age %u", provider.first.c_str(), now, key_response_age);
provider.second.status_sensor->publish_state(true);
}
#endif
}
}
} }
void PacketTransport::add_key_(const char *name, uint32_t key) { void PacketTransport::add_key_(const char *name, uint32_t key) {
@ -437,7 +466,8 @@ void PacketTransport::process_(const std::vector<uint8_t> &data) {
if (decoder.decode(PING_KEY, key) == DECODE_OK) { if (decoder.decode(PING_KEY, key) == DECODE_OK) {
if (key == this->ping_key_) { if (key == this->ping_key_) {
ping_key_seen = true; ping_key_seen = true;
ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key); provider.last_key_response_time = millis() / 1000;
ESP_LOGV(TAG, "Found good ping key %X at timestamp %" PRIu32, (unsigned) key, provider.last_key_response_time);
} else { } else {
ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key);
} }

View File

@ -8,7 +8,7 @@
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#endif #endif
#
#include <vector> #include <vector>
#include <map> #include <map>
@ -27,6 +27,10 @@ struct Provider {
std::vector<uint8_t> encryption_key; std::vector<uint8_t> encryption_key;
const char *name; const char *name;
uint32_t last_code[2]; uint32_t last_code[2];
uint32_t last_key_response_time;
#ifdef USE_STATUS_SENSOR
binary_sensor::BinarySensor *status_sensor{nullptr};
#endif
}; };
#ifdef USE_SENSOR #ifdef USE_SENSOR
@ -75,10 +79,7 @@ class PacketTransport : public PollingComponent {
void add_provider(const char *hostname) { void add_provider(const char *hostname) {
if (this->providers_.count(hostname) == 0) { if (this->providers_.count(hostname) == 0) {
Provider provider; Provider provider{};
provider.encryption_key = std::vector<uint8_t>{};
provider.last_code[0] = 0;
provider.last_code[1] = 0;
provider.name = hostname; provider.name = hostname;
this->providers_[hostname] = provider; this->providers_[hostname] = provider;
#ifdef USE_SENSOR #ifdef USE_SENSOR
@ -97,6 +98,11 @@ class PacketTransport : public PollingComponent {
void set_provider_encryption(const char *name, std::vector<uint8_t> key) { void set_provider_encryption(const char *name, std::vector<uint8_t> key) {
this->providers_[name].encryption_key = std::move(key); this->providers_[name].encryption_key = std::move(key);
} }
#ifdef USE_STATUS_SENSOR
void set_provider_status_sensor(const char *name, binary_sensor::BinarySensor *sensor) {
this->providers_[name].status_sensor = sensor;
}
#endif
void set_platform_name(const char *name) { this->platform_name_ = name; } void set_platform_name(const char *name) { this->platform_name_ = name; }
protected: protected:

View File

@ -0,0 +1,325 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import spi
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
MULTI_CONF = True
CODEOWNERS = ["@swoboda1337"]
DEPENDENCIES = ["spi"]
CONF_SX127X_ID = "sx127x_id"
CONF_AUTO_CAL = "auto_cal"
CONF_BANDWIDTH = "bandwidth"
CONF_BITRATE = "bitrate"
CONF_BITSYNC = "bitsync"
CONF_CODING_RATE = "coding_rate"
CONF_CRC_ENABLE = "crc_enable"
CONF_DEVIATION = "deviation"
CONF_DIO0_PIN = "dio0_pin"
CONF_MODULATION = "modulation"
CONF_ON_PACKET = "on_packet"
CONF_PA_PIN = "pa_pin"
CONF_PA_POWER = "pa_power"
CONF_PA_RAMP = "pa_ramp"
CONF_PACKET_MODE = "packet_mode"
CONF_PAYLOAD_LENGTH = "payload_length"
CONF_PREAMBLE_DETECT = "preamble_detect"
CONF_PREAMBLE_ERRORS = "preamble_errors"
CONF_PREAMBLE_POLARITY = "preamble_polarity"
CONF_PREAMBLE_SIZE = "preamble_size"
CONF_RST_PIN = "rst_pin"
CONF_RX_FLOOR = "rx_floor"
CONF_RX_START = "rx_start"
CONF_SHAPING = "shaping"
CONF_SPREADING_FACTOR = "spreading_factor"
CONF_SYNC_VALUE = "sync_value"
sx127x_ns = cg.esphome_ns.namespace("sx127x")
SX127x = sx127x_ns.class_("SX127x", cg.Component, spi.SPIDevice)
SX127xListener = sx127x_ns.class_("SX127xListener")
SX127xBw = sx127x_ns.enum("SX127xBw")
SX127xOpMode = sx127x_ns.enum("SX127xOpMode")
SX127xPaConfig = sx127x_ns.enum("SX127xPaConfig")
SX127xPaRamp = sx127x_ns.enum("SX127xPaRamp")
SX127xModemCfg1 = sx127x_ns.enum("SX127xModemCfg1")
BW = {
"2_6kHz": SX127xBw.SX127X_BW_2_6,
"3_1kHz": SX127xBw.SX127X_BW_3_1,
"3_9kHz": SX127xBw.SX127X_BW_3_9,
"5_2kHz": SX127xBw.SX127X_BW_5_2,
"6_3kHz": SX127xBw.SX127X_BW_6_3,
"7_8kHz": SX127xBw.SX127X_BW_7_8,
"10_4kHz": SX127xBw.SX127X_BW_10_4,
"12_5kHz": SX127xBw.SX127X_BW_12_5,
"15_6kHz": SX127xBw.SX127X_BW_15_6,
"20_8kHz": SX127xBw.SX127X_BW_20_8,
"25_0kHz": SX127xBw.SX127X_BW_25_0,
"31_3kHz": SX127xBw.SX127X_BW_31_3,
"41_7kHz": SX127xBw.SX127X_BW_41_7,
"50_0kHz": SX127xBw.SX127X_BW_50_0,
"62_5kHz": SX127xBw.SX127X_BW_62_5,
"83_3kHz": SX127xBw.SX127X_BW_83_3,
"100_0kHz": SX127xBw.SX127X_BW_100_0,
"125_0kHz": SX127xBw.SX127X_BW_125_0,
"166_7kHz": SX127xBw.SX127X_BW_166_7,
"200_0kHz": SX127xBw.SX127X_BW_200_0,
"250_0kHz": SX127xBw.SX127X_BW_250_0,
"500_0kHz": SX127xBw.SX127X_BW_500_0,
}
CODING_RATE = {
"CR_4_5": SX127xModemCfg1.CODING_RATE_4_5,
"CR_4_6": SX127xModemCfg1.CODING_RATE_4_6,
"CR_4_7": SX127xModemCfg1.CODING_RATE_4_7,
"CR_4_8": SX127xModemCfg1.CODING_RATE_4_8,
}
MOD = {
"LORA": SX127xOpMode.MOD_LORA,
"FSK": SX127xOpMode.MOD_FSK,
"OOK": SX127xOpMode.MOD_OOK,
}
PA_PIN = {
"RFO": SX127xPaConfig.PA_PIN_RFO,
"BOOST": SX127xPaConfig.PA_PIN_BOOST,
}
RAMP = {
"10us": SX127xPaRamp.PA_RAMP_10,
"12us": SX127xPaRamp.PA_RAMP_12,
"15us": SX127xPaRamp.PA_RAMP_15,
"20us": SX127xPaRamp.PA_RAMP_20,
"25us": SX127xPaRamp.PA_RAMP_25,
"31us": SX127xPaRamp.PA_RAMP_31,
"40us": SX127xPaRamp.PA_RAMP_40,
"50us": SX127xPaRamp.PA_RAMP_50,
"62us": SX127xPaRamp.PA_RAMP_62,
"100us": SX127xPaRamp.PA_RAMP_100,
"125us": SX127xPaRamp.PA_RAMP_125,
"250us": SX127xPaRamp.PA_RAMP_250,
"500us": SX127xPaRamp.PA_RAMP_500,
"1000us": SX127xPaRamp.PA_RAMP_1000,
"2000us": SX127xPaRamp.PA_RAMP_2000,
"3400us": SX127xPaRamp.PA_RAMP_3400,
}
SHAPING = {
"CUTOFF_BR_X_2": SX127xPaRamp.CUTOFF_BR_X_2,
"CUTOFF_BR_X_1": SX127xPaRamp.CUTOFF_BR_X_1,
"GAUSSIAN_BT_0_3": SX127xPaRamp.GAUSSIAN_BT_0_3,
"GAUSSIAN_BT_0_5": SX127xPaRamp.GAUSSIAN_BT_0_5,
"GAUSSIAN_BT_1_0": SX127xPaRamp.GAUSSIAN_BT_1_0,
"NONE": SX127xPaRamp.SHAPING_NONE,
}
RunImageCalAction = sx127x_ns.class_(
"RunImageCalAction", automation.Action, cg.Parented.template(SX127x)
)
SendPacketAction = sx127x_ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(SX127x)
)
SetModeTxAction = sx127x_ns.class_(
"SetModeTxAction", automation.Action, cg.Parented.template(SX127x)
)
SetModeRxAction = sx127x_ns.class_(
"SetModeRxAction", automation.Action, cg.Parented.template(SX127x)
)
SetModeSleepAction = sx127x_ns.class_(
"SetModeSleepAction", automation.Action, cg.Parented.template(SX127x)
)
SetModeStandbyAction = sx127x_ns.class_(
"SetModeStandbyAction", automation.Action, cg.Parented.template(SX127x)
)
def validate_raw_data(value):
if isinstance(value, str):
return value.encode("utf-8")
if isinstance(value, list):
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
"data must either be a string wrapped in quotes or a list of bytes"
)
def validate_config(config):
if config[CONF_MODULATION] == "LORA":
bws = [
"7_8kHz",
"10_4kHz",
"15_6kHz",
"20_8kHz",
"31_3kHz",
"41_7kHz",
"62_5kHz",
"125_0kHz",
"250_0kHz",
"500_0kHz",
]
if config[CONF_BANDWIDTH] not in bws:
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA")
if CONF_DIO0_PIN not in config:
raise cv.Invalid("Cannot use LoRa without dio0_pin")
if 0 < config[CONF_PREAMBLE_SIZE] < 6:
raise cv.Invalid("Minimum preamble size is 6 with LORA")
if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0:
raise cv.Invalid("Payload length must be set when spreading factor is 6")
else:
if config[CONF_BANDWIDTH] == "500_0kHz":
raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is only available with LORA")
if CONF_BITSYNC not in config:
raise cv.Invalid("Config 'bitsync' required with FSK/OOK")
if CONF_PACKET_MODE not in config:
raise cv.Invalid("Config 'packet_mode' required with FSK/OOK")
if config[CONF_PACKET_MODE] and CONF_DIO0_PIN not in config:
raise cv.Invalid("Config 'dio0_pin' required in packet mode")
if config[CONF_PAYLOAD_LENGTH] > 64:
raise cv.Invalid("Payload length must be <= 64 with FSK/OOK")
if config[CONF_PA_PIN] == "RFO" and config[CONF_PA_POWER] > 15:
raise cv.Invalid("PA power must be <= 15 dbm when using the RFO pin")
if config[CONF_PA_PIN] == "BOOST" and config[CONF_PA_POWER] < 2:
raise cv.Invalid("PA power must be >= 2 dbm when using the BOOST pin")
return config
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SX127x),
cv.Optional(CONF_AUTO_CAL, default=True): cv.boolean,
cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=500, max=300000),
cv.Optional(CONF_BITSYNC): cv.boolean,
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN),
cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=0, max=17),
cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP),
cv.Optional(CONF_PACKET_MODE): cv.boolean,
cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
cv.Optional(CONF_PREAMBLE_DETECT, default=0): cv.int_range(min=0, max=3),
cv.Optional(CONF_PREAMBLE_ERRORS, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_PREAMBLE_POLARITY, default=0xAA): cv.All(
cv.hex_int, cv.one_of(0xAA, 0x55)
),
cv.Optional(CONF_PREAMBLE_SIZE, default=0): cv.int_range(min=0, max=65535),
cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_RX_FLOOR, default=-94): cv.float_range(min=-128, max=-1),
cv.Optional(CONF_RX_START, default=True): cv.boolean,
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
},
)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema(True, 8e6, "mode0"))
.add_extra(validate_config)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
if CONF_ON_PACKET in config:
await automation.build_automation(
var.get_packet_trigger(),
[
(cg.std_vector.template(cg.uint8), "x"),
(cg.float_, "rssi"),
(cg.float_, "snr"),
],
config[CONF_ON_PACKET],
)
if CONF_DIO0_PIN in config:
dio0_pin = await cg.gpio_pin_expression(config[CONF_DIO0_PIN])
cg.add(var.set_dio0_pin(dio0_pin))
rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN])
cg.add(var.set_rst_pin(rst_pin))
cg.add(var.set_auto_cal(config[CONF_AUTO_CAL]))
cg.add(var.set_bandwidth(config[CONF_BANDWIDTH]))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
cg.add(var.set_deviation(config[CONF_DEVIATION]))
cg.add(var.set_modulation(config[CONF_MODULATION]))
if config[CONF_MODULATION] != "LORA":
cg.add(var.set_bitrate(config[CONF_BITRATE]))
cg.add(var.set_bitsync(config[CONF_BITSYNC]))
cg.add(var.set_packet_mode(config[CONF_PACKET_MODE]))
cg.add(var.set_pa_pin(config[CONF_PA_PIN]))
cg.add(var.set_pa_ramp(config[CONF_PA_RAMP]))
cg.add(var.set_pa_power(config[CONF_PA_POWER]))
cg.add(var.set_shaping(config[CONF_SHAPING]))
cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE]))
cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH]))
cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT]))
cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE]))
cg.add(var.set_preamble_polarity(config[CONF_PREAMBLE_POLARITY]))
cg.add(var.set_preamble_errors(config[CONF_PREAMBLE_ERRORS]))
cg.add(var.set_coding_rate(config[CONF_CODING_RATE]))
cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR]))
cg.add(var.set_sync_value(config[CONF_SYNC_VALUE]))
cg.add(var.set_rx_floor(config[CONF_RX_FLOOR]))
cg.add(var.set_rx_start(config[CONF_RX_START]))
NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(SX127x),
}
)
@automation.register_action(
"sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA
)
@automation.register_action(
"sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA
)
async def no_args_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SX127x),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
)
@automation.register_action(
"sx127x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
)
async def send_packet_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_DATA]
if isinstance(data, bytes):
data = list(data)
if cg.is_template(data):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
cg.add(var.set_data_static(data))
return var

View File

@ -0,0 +1,62 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sx127x/sx127x.h"
namespace esphome {
namespace sx127x {
template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public Parented<SX127x> {
public:
void play(Ts... x) override { this->parent_->run_image_cal(); }
};
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX127x> {
public:
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(Ts... x) override {
if (this->static_) {
this->parent_->transmit_packet(this->data_static_);
} else {
this->parent_->transmit_packet(this->data_func_(x...));
}
}
protected:
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX127x> {
public:
void play(Ts... x) override { this->parent_->set_mode_tx(); }
};
template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Parented<SX127x> {
public:
void play(Ts... x) override { this->parent_->set_mode_rx(); }
};
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX127x> {
public:
void play(Ts... x) override { this->parent_->set_mode_sleep(); }
};
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX127x> {
public:
void play(Ts... x) override { this->parent_->set_mode_standby(); }
};
} // namespace sx127x
} // namespace esphome

View File

@ -0,0 +1,26 @@
import esphome.codegen as cg
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
import esphome.config_validation as cv
from esphome.cpp_types import PollingComponent
from .. import CONF_SX127X_ID, SX127x, SX127xListener, sx127x_ns
SX127xTransport = sx127x_ns.class_(
"SX127xTransport", PacketTransport, PollingComponent, SX127xListener
)
CONFIG_SCHEMA = transport_schema(SX127xTransport).extend(
{
cv.GenerateID(CONF_SX127X_ID): cv.use_id(SX127x),
}
)
async def to_code(config):
var, _ = await new_packet_transport(config)
sx127x = await cg.get_variable(config[CONF_SX127X_ID])
cg.add(var.set_parent(sx127x))

View File

@ -0,0 +1,26 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "sx127x_transport.h"
namespace esphome {
namespace sx127x {
static const char *const TAG = "sx127x_transport";
void SX127xTransport::setup() {
PacketTransport::setup();
this->parent_->register_listener(this);
}
void SX127xTransport::update() {
PacketTransport::update();
this->updated_ = true;
this->resend_data_ = true;
}
void SX127xTransport::send_packet(const std::vector<uint8_t> &buf) const { this->parent_->transmit_packet(buf); }
void SX127xTransport::on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) { this->process_(packet); }
} // namespace sx127x
} // namespace esphome

View File

@ -0,0 +1,25 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sx127x/sx127x.h"
#include "esphome/components/packet_transport/packet_transport.h"
#include <vector>
namespace esphome {
namespace sx127x {
class SX127xTransport : public packet_transport::PacketTransport, public Parented<SX127x>, public SX127xListener {
public:
void setup() override;
void update() override;
void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; }
size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); }
};
} // namespace sx127x
} // namespace esphome

View File

@ -0,0 +1,493 @@
#include "sx127x.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace sx127x {
static const char *const TAG = "sx127x";
static const uint32_t FXOSC = 32000000u;
static const uint16_t RAMP[16] = {3400, 2000, 1000, 500, 250, 125, 100, 62, 50, 40, 31, 25, 20, 15, 12, 10};
static const uint32_t BW_HZ[22] = {2604, 3125, 3906, 5208, 6250, 7812, 10416, 12500, 15625, 20833, 25000,
31250, 41666, 50000, 62500, 83333, 100000, 125000, 166666, 200000, 250000, 500000};
static const uint8_t BW_LORA[22] = {BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_10_4, BW_15_6,
BW_15_6, BW_20_8, BW_31_3, BW_31_3, BW_41_7, BW_62_5, BW_62_5, BW_125_0,
BW_125_0, BW_125_0, BW_250_0, BW_250_0, BW_250_0, BW_500_0};
static const uint8_t BW_FSK_OOK[22] = {RX_BW_2_6, RX_BW_3_1, RX_BW_3_9, RX_BW_5_2, RX_BW_6_3, RX_BW_7_8,
RX_BW_10_4, RX_BW_12_5, RX_BW_15_6, RX_BW_20_8, RX_BW_25_0, RX_BW_31_3,
RX_BW_41_7, RX_BW_50_0, RX_BW_62_5, RX_BW_83_3, RX_BW_100_0, RX_BW_125_0,
RX_BW_166_7, RX_BW_200_0, RX_BW_250_0, RX_BW_250_0};
static const int32_t RSSI_OFFSET_HF = 157;
static const int32_t RSSI_OFFSET_LF = 164;
uint8_t SX127x::read_register_(uint8_t reg) {
this->enable();
this->write_byte(reg & 0x7F);
uint8_t value = this->read_byte();
this->disable();
return value;
}
void SX127x::write_register_(uint8_t reg, uint8_t value) {
this->enable();
this->write_byte(reg | 0x80);
this->write_byte(value);
this->disable();
}
void SX127x::read_fifo_(std::vector<uint8_t> &packet) {
this->enable();
this->write_byte(REG_FIFO & 0x7F);
this->read_array(packet.data(), packet.size());
this->disable();
}
void SX127x::write_fifo_(const std::vector<uint8_t> &packet) {
this->enable();
this->write_byte(REG_FIFO | 0x80);
this->write_array(packet.data(), packet.size());
this->disable();
}
void SX127x::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
// setup reset
this->rst_pin_->setup();
// setup dio0
if (this->dio0_pin_) {
this->dio0_pin_->setup();
}
// start spi
this->spi_setup();
// configure rf
this->configure();
}
void SX127x::configure() {
// toggle chip reset
this->rst_pin_->digital_write(false);
delayMicroseconds(1000);
this->rst_pin_->digital_write(true);
delayMicroseconds(10000);
// check silicon version to make sure hw is ok
if (this->read_register_(REG_VERSION) != 0x12) {
this->mark_failed();
return;
}
// enter sleep mode
this->set_mode_(MOD_FSK, MODE_SLEEP);
// set freq
uint64_t frf = ((uint64_t) this->frequency_ << 19) / FXOSC;
this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
this->write_register_(REG_FRF_LSB, (uint8_t) ((frf >> 0) & 0xFF));
// enter standby mode
this->set_mode_(MOD_FSK, MODE_STDBY);
// run image cal
this->run_image_cal();
// go back to sleep
this->set_mode_sleep();
// config pa
if (this->pa_pin_ == PA_PIN_BOOST) {
this->pa_power_ = std::max(this->pa_power_, (uint8_t) 2);
this->pa_power_ = std::min(this->pa_power_, (uint8_t) 17);
this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 2) | this->pa_pin_ | PA_MAX_POWER);
} else {
this->pa_power_ = std::min(this->pa_power_, (uint8_t) 14);
this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 0) | this->pa_pin_ | PA_MAX_POWER);
}
if (this->modulation_ != MOD_LORA) {
this->write_register_(REG_PA_RAMP, this->pa_ramp_ | this->shaping_);
} else {
this->write_register_(REG_PA_RAMP, this->pa_ramp_);
}
// configure modem
if (this->modulation_ != MOD_LORA) {
this->configure_fsk_ook_();
} else {
this->configure_lora_();
}
// switch to rx or sleep
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
}
void SX127x::configure_fsk_ook_() {
// set the channel bw
this->write_register_(REG_RX_BW, BW_FSK_OOK[this->bandwidth_]);
// set fdev
uint32_t fdev = std::min((this->deviation_ * 4096) / 250000, (uint32_t) 0x3FFF);
this->write_register_(REG_FDEV_MSB, (uint8_t) ((fdev >> 8) & 0xFF));
this->write_register_(REG_FDEV_LSB, (uint8_t) ((fdev >> 0) & 0xFF));
// set bitrate
uint64_t bitrate = (FXOSC + this->bitrate_ / 2) / this->bitrate_; // round up
this->write_register_(REG_BITRATE_MSB, (uint8_t) ((bitrate >> 8) & 0xFF));
this->write_register_(REG_BITRATE_LSB, (uint8_t) ((bitrate >> 0) & 0xFF));
// configure rx and afc
uint8_t trigger = (this->preamble_detect_ > 0) ? TRIGGER_PREAMBLE : TRIGGER_RSSI;
this->write_register_(REG_AFC_FEI, AFC_AUTO_CLEAR_ON);
if (this->modulation_ == MOD_FSK) {
this->write_register_(REG_RX_CONFIG, AFC_AUTO_ON | AGC_AUTO_ON | trigger);
} else {
this->write_register_(REG_RX_CONFIG, AGC_AUTO_ON | trigger);
}
// configure packet mode
if (this->packet_mode_) {
uint8_t crc_mode = (this->crc_enable_) ? CRC_ON : CRC_OFF;
this->write_register_(REG_FIFO_THRESH, TX_START_FIFO_EMPTY);
if (this->payload_length_ > 0) {
this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->payload_length_);
this->write_register_(REG_PACKET_CONFIG_1, crc_mode | FIXED_LENGTH);
} else {
this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->get_max_packet_size() - 1);
this->write_register_(REG_PACKET_CONFIG_1, crc_mode | VARIABLE_LENGTH);
}
this->write_register_(REG_PACKET_CONFIG_2, PACKET_MODE);
} else {
this->write_register_(REG_PACKET_CONFIG_2, CONTINUOUS_MODE);
}
this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00);
// config bit synchronizer
uint8_t polarity = (this->preamble_polarity_ == 0xAA) ? PREAMBLE_AA : PREAMBLE_55;
if (!this->sync_value_.empty()) {
uint8_t size = this->sync_value_.size() - 1;
this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity | SYNC_ON | size);
for (uint32_t i = 0; i < this->sync_value_.size(); i++) {
this->write_register_(REG_SYNC_VALUE1 + i, this->sync_value_[i]);
}
} else {
this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity);
}
// config preamble detector
if (this->preamble_detect_ > 0) {
uint8_t size = (this->preamble_detect_ - 1) << PREAMBLE_DETECTOR_SIZE_SHIFT;
uint8_t tol = this->preamble_errors_ << PREAMBLE_DETECTOR_TOL_SHIFT;
this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_ON | size | tol);
} else {
this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_OFF);
}
this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 16);
this->write_register_(REG_PREAMBLE_SIZE_LSB, this->preamble_size_ & 0xFF);
// config sync generation and setup ook threshold
uint8_t bitsync = this->bitsync_ ? BIT_SYNC_ON : BIT_SYNC_OFF;
this->write_register_(REG_OOK_PEAK, bitsync | OOK_THRESH_STEP_0_5 | OOK_THRESH_PEAK);
this->write_register_(REG_OOK_AVG, OOK_AVG_RESERVED | OOK_THRESH_DEC_1_8);
// set rx floor
this->write_register_(REG_OOK_FIX, 256 + int(this->rx_floor_ * 2.0));
this->write_register_(REG_RSSI_THRESH, std::abs(int(this->rx_floor_ * 2.0)));
}
void SX127x::configure_lora_() {
// config modem
uint8_t header_mode = this->payload_length_ > 0 ? IMPLICIT_HEADER : EXPLICIT_HEADER;
uint8_t crc_mode = (this->crc_enable_) ? RX_PAYLOAD_CRC_ON : RX_PAYLOAD_CRC_OFF;
uint8_t spreading_factor = this->spreading_factor_ << SPREADING_FACTOR_SHIFT;
this->write_register_(REG_MODEM_CONFIG1, BW_LORA[this->bandwidth_] | this->coding_rate_ | header_mode);
this->write_register_(REG_MODEM_CONFIG2, spreading_factor | crc_mode);
// config fifo and payload length
this->write_register_(REG_FIFO_TX_BASE_ADDR, 0x00);
this->write_register_(REG_FIFO_RX_BASE_ADDR, 0x00);
this->write_register_(REG_PAYLOAD_LENGTH, std::max(this->payload_length_, (uint32_t) 1));
// config preamble
if (this->preamble_size_ >= 6) {
this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 16);
this->write_register_(REG_PREAMBLE_LEN_LSB, this->preamble_size_ & 0xFF);
}
// optimize detection
float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_];
if (duration > 16) {
this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON | LOW_DATA_RATE_OPTIMIZE_ON);
} else {
this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON);
}
if (this->spreading_factor_ == 6) {
this->write_register_(REG_DETECT_OPTIMIZE, 0xC5);
this->write_register_(REG_DETECT_THRESHOLD, 0x0C);
} else {
this->write_register_(REG_DETECT_OPTIMIZE, 0xC3);
this->write_register_(REG_DETECT_THRESHOLD, 0x0A);
}
// config sync word
if (!this->sync_value_.empty()) {
this->write_register_(REG_SYNC_WORD, this->sync_value_[0]);
}
}
size_t SX127x::get_max_packet_size() {
if (this->payload_length_ > 0) {
return this->payload_length_;
}
if (this->modulation_ == MOD_LORA) {
return 256;
} else {
return 64;
}
}
void SX127x::transmit_packet(const std::vector<uint8_t> &packet) {
if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) {
ESP_LOGE(TAG, "Packet size does not match config");
return;
}
if (packet.empty() || packet.size() > this->get_max_packet_size()) {
ESP_LOGE(TAG, "Packet size out of range");
return;
}
if (this->modulation_ == MOD_LORA) {
this->set_mode_standby();
if (this->payload_length_ == 0) {
this->write_register_(REG_PAYLOAD_LENGTH, packet.size());
}
this->write_register_(REG_IRQ_FLAGS, 0xFF);
this->write_register_(REG_FIFO_ADDR_PTR, 0);
this->write_fifo_(packet);
this->set_mode_tx();
} else {
this->set_mode_standby();
if (this->payload_length_ == 0) {
this->write_register_(REG_FIFO, packet.size());
}
this->write_fifo_(packet);
this->set_mode_tx();
}
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio0_pin_->digital_read()) {
if (millis() - start > 4000) {
ESP_LOGE(TAG, "Transmit packet failure");
break;
}
}
if (this->rx_start_) {
this->set_mode_rx();
} else {
this->set_mode_sleep();
}
}
void SX127x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, rssi, snr);
}
this->packet_trigger_->trigger(packet, rssi, snr);
}
void SX127x::loop() {
if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) {
return;
}
if (this->modulation_ == MOD_LORA) {
uint8_t status = this->read_register_(REG_IRQ_FLAGS);
this->write_register_(REG_IRQ_FLAGS, 0xFF);
if ((status & PAYLOAD_CRC_ERROR) == 0) {
uint8_t bytes = this->read_register_(REG_NB_RX_BYTES);
uint8_t addr = this->read_register_(REG_FIFO_RX_CURR_ADDR);
uint8_t rssi = this->read_register_(REG_PKT_RSSI_VALUE);
int8_t snr = (int8_t) this->read_register_(REG_PKT_SNR_VALUE);
std::vector<uint8_t> packet(bytes);
this->write_register_(REG_FIFO_ADDR_PTR, addr);
this->read_fifo_(packet);
if (this->frequency_ > 700000000) {
this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4);
} else {
this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4);
}
}
} else if (this->packet_mode_) {
std::vector<uint8_t> packet;
uint8_t payload_length = this->payload_length_;
if (payload_length == 0) {
payload_length = this->read_register_(REG_FIFO);
}
packet.resize(payload_length);
this->read_fifo_(packet);
this->call_listeners_(packet, 0.0f, 0.0f);
}
}
void SX127x::run_image_cal() {
uint32_t start = millis();
uint8_t mode = this->read_register_(REG_OP_MODE);
if ((mode & MODE_MASK) != MODE_STDBY) {
ESP_LOGE(TAG, "Need to be in standby for image cal");
return;
}
if (mode & MOD_LORA) {
this->set_mode_(MOD_FSK, MODE_SLEEP);
this->set_mode_(MOD_FSK, MODE_STDBY);
}
if (this->auto_cal_) {
this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START | AUTO_IMAGE_CAL_ON | TEMP_THRESHOLD_10C);
} else {
this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START);
}
while (this->read_register_(REG_IMAGE_CAL) & IMAGE_CAL_RUNNING) {
if (millis() - start > 20) {
ESP_LOGE(TAG, "Image cal failure");
break;
}
}
if (mode & MOD_LORA) {
this->set_mode_(this->modulation_, MODE_SLEEP);
this->set_mode_(this->modulation_, MODE_STDBY);
}
}
void SX127x::set_mode_(uint8_t modulation, uint8_t mode) {
uint32_t start = millis();
this->write_register_(REG_OP_MODE, modulation | mode);
while (true) {
uint8_t curr = this->read_register_(REG_OP_MODE) & MODE_MASK;
if ((curr == mode) || (mode == MODE_RX && curr == MODE_RX_FS)) {
if (mode == MODE_SLEEP) {
this->write_register_(REG_OP_MODE, modulation | mode);
}
break;
}
if (millis() - start > 20) {
ESP_LOGE(TAG, "Set mode failure");
break;
}
}
}
void SX127x::set_mode_rx() {
this->set_mode_(this->modulation_, MODE_RX);
if (this->modulation_ == MOD_LORA) {
this->write_register_(REG_IRQ_FLAGS_MASK, 0x00);
this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00);
}
}
void SX127x::set_mode_tx() {
this->set_mode_(this->modulation_, MODE_TX);
if (this->modulation_ == MOD_LORA) {
this->write_register_(REG_IRQ_FLAGS_MASK, 0x00);
this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_01);
}
}
void SX127x::set_mode_standby() { this->set_mode_(this->modulation_, MODE_STDBY); }
void SX127x::set_mode_sleep() { this->set_mode_(this->modulation_, MODE_SLEEP); }
void SX127x::dump_config() {
ESP_LOGCONFIG(TAG, "SX127x:");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" RST Pin: ", this->rst_pin_);
LOG_PIN(" DIO0 Pin: ", this->dio0_pin_);
const char *shaping = "NONE";
if (this->shaping_ == CUTOFF_BR_X_2) {
shaping = "CUTOFF_BR_X_2";
} else if (this->shaping_ == CUTOFF_BR_X_1) {
shaping = "CUTOFF_BR_X_1";
} else if (this->shaping_ == GAUSSIAN_BT_0_3) {
shaping = "GAUSSIAN_BT_0_3";
} else if (this->shaping_ == GAUSSIAN_BT_0_5) {
shaping = "GAUSSIAN_BT_0_5";
} else if (this->shaping_ == GAUSSIAN_BT_1_0) {
shaping = "GAUSSIAN_BT_1_0";
}
const char *pa_pin = "RFO";
if (this->pa_pin_ == PA_PIN_BOOST) {
pa_pin = "BOOST";
}
ESP_LOGCONFIG(TAG,
" Auto Cal: %s\n"
" Frequency: %" PRIu32 " Hz\n"
" Bandwidth: %" PRIu32 " Hz\n"
" PA Pin: %s\n"
" PA Power: %" PRIu8 " dBm\n"
" PA Ramp: %" PRIu16 " us\n"
" Shaping: %s",
TRUEFALSE(this->auto_cal_), this->frequency_, BW_HZ[this->bandwidth_], pa_pin, this->pa_power_,
RAMP[this->pa_ramp_], shaping);
if (this->modulation_ == MOD_FSK) {
ESP_LOGCONFIG(TAG, " Deviation: %" PRIu32 " Hz", this->deviation_);
}
if (this->modulation_ == MOD_LORA) {
const char *cr = "4/8";
if (this->coding_rate_ == CODING_RATE_4_5) {
cr = "4/5";
} else if (this->coding_rate_ == CODING_RATE_4_6) {
cr = "4/6";
} else if (this->coding_rate_ == CODING_RATE_4_7) {
cr = "4/7";
}
ESP_LOGCONFIG(TAG,
" Modulation: LORA\n"
" Preamble Size: %" PRIu16 "\n"
" Spreading Factor: %" PRIu8 "\n"
" Coding Rate: %s\n"
" CRC Enable: %s",
this->preamble_size_, this->spreading_factor_, cr, TRUEFALSE(this->crc_enable_));
if (this->payload_length_ > 0) {
ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_);
}
if (!this->sync_value_.empty()) {
ESP_LOGCONFIG(TAG, " Sync Value: 0x%02x", this->sync_value_[0]);
}
} else {
ESP_LOGCONFIG(TAG,
" Modulation: %s\n"
" Bitrate: %" PRIu32 "b/s\n"
" Bitsync: %s\n"
" Rx Start: %s\n"
" Rx Floor: %.1f dBm\n"
" Packet Mode: %s",
this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_),
TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_));
if (this->packet_mode_) {
ESP_LOGCONFIG(TAG, " CRC Enable: %s", TRUEFALSE(this->crc_enable_));
}
if (this->payload_length_ > 0) {
ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_);
}
if (!this->sync_value_.empty()) {
ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str());
}
if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) {
ESP_LOGCONFIG(TAG,
" Preamble Polarity: 0x%X\n"
" Preamble Size: %" PRIu16 "\n"
" Preamble Detect: %" PRIu8 "\n"
" Preamble Errors: %" PRIu8,
this->preamble_polarity_, this->preamble_size_, this->preamble_detect_, this->preamble_errors_);
}
}
if (this->is_failed()) {
ESP_LOGE(TAG, "Configuring SX127x failed");
}
}
} // namespace sx127x
} // namespace esphome

View File

@ -0,0 +1,125 @@
#pragma once
#include "sx127x_reg.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include <vector>
namespace esphome {
namespace sx127x {
enum SX127xBw : uint8_t {
SX127X_BW_2_6,
SX127X_BW_3_1,
SX127X_BW_3_9,
SX127X_BW_5_2,
SX127X_BW_6_3,
SX127X_BW_7_8,
SX127X_BW_10_4,
SX127X_BW_12_5,
SX127X_BW_15_6,
SX127X_BW_20_8,
SX127X_BW_25_0,
SX127X_BW_31_3,
SX127X_BW_41_7,
SX127X_BW_50_0,
SX127X_BW_62_5,
SX127X_BW_83_3,
SX127X_BW_100_0,
SX127X_BW_125_0,
SX127X_BW_166_7,
SX127X_BW_200_0,
SX127X_BW_250_0,
SX127X_BW_500_0,
};
class SX127xListener {
public:
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
};
class SX127x : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_8MHZ> {
public:
size_t get_max_packet_size();
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void setup() override;
void loop() override;
void dump_config() override;
void set_auto_cal(bool auto_cal) { this->auto_cal_ = auto_cal; }
void set_bandwidth(SX127xBw bandwidth) { this->bandwidth_ = bandwidth; }
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
void set_bitsync(bool bitsync) { this->bitsync_ = bitsync; }
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
void set_dio0_pin(InternalGPIOPin *dio0_pin) { this->dio0_pin_ = dio0_pin; }
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
void set_mode_rx();
void set_mode_tx();
void set_mode_standby();
void set_mode_sleep();
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
void set_pa_pin(uint8_t pin) { this->pa_pin_ = pin; }
void set_pa_power(uint8_t power) { this->pa_power_ = power; }
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
void set_packet_mode(bool packet_mode) { this->packet_mode_ = packet_mode; }
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
void set_preamble_errors(uint8_t preamble_errors) { this->preamble_errors_ = preamble_errors; }
void set_preamble_polarity(uint8_t preamble_polarity) { this->preamble_polarity_ = preamble_polarity; }
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
void set_preamble_detect(uint8_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
void set_rx_floor(float floor) { this->rx_floor_ = floor; }
void set_rx_start(bool start) { this->rx_start_ = start; }
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->sync_value_ = sync_value; }
void run_image_cal();
void configure();
void transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() const { return this->packet_trigger_; };
protected:
void configure_fsk_ook_();
void configure_lora_();
void set_mode_(uint8_t modulation, uint8_t mode);
void write_fifo_(const std::vector<uint8_t> &packet);
void read_fifo_(std::vector<uint8_t> &packet);
void write_register_(uint8_t reg, uint8_t value);
void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr);
uint8_t read_register_(uint8_t reg);
Trigger<std::vector<uint8_t>, float, float> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, float>()};
std::vector<SX127xListener *> listeners_;
std::vector<uint8_t> sync_value_;
InternalGPIOPin *dio0_pin_{nullptr};
InternalGPIOPin *rst_pin_{nullptr};
SX127xBw bandwidth_;
uint32_t bitrate_;
uint32_t deviation_;
uint32_t frequency_;
uint32_t payload_length_;
uint16_t preamble_size_;
uint8_t coding_rate_;
uint8_t modulation_;
uint8_t pa_pin_;
uint8_t pa_power_;
uint8_t pa_ramp_;
uint8_t preamble_detect_;
uint8_t preamble_errors_;
uint8_t preamble_polarity_;
uint8_t shaping_;
uint8_t spreading_factor_;
float rx_floor_;
bool auto_cal_{false};
bool bitsync_{false};
bool crc_enable_{false};
bool packet_mode_{false};
bool rx_start_{false};
};
} // namespace sx127x
} // namespace esphome

View File

@ -0,0 +1,295 @@
#pragma once
#include "esphome/core/hal.h"
namespace esphome {
namespace sx127x {
enum SX127xReg : uint8_t {
// Common registers
REG_FIFO = 0x00,
REG_OP_MODE = 0x01,
REG_BITRATE_MSB = 0x02,
REG_BITRATE_LSB = 0x03,
REG_FDEV_MSB = 0x04,
REG_FDEV_LSB = 0x05,
REG_FRF_MSB = 0x06,
REG_FRF_MID = 0x07,
REG_FRF_LSB = 0x08,
REG_PA_CONFIG = 0x09,
REG_PA_RAMP = 0x0A,
REG_DIO_MAPPING1 = 0x40,
REG_DIO_MAPPING2 = 0x41,
REG_VERSION = 0x42,
// FSK/OOK registers
REG_RX_CONFIG = 0x0D,
REG_RSSI_THRESH = 0x10,
REG_RX_BW = 0x12,
REG_OOK_PEAK = 0x14,
REG_OOK_FIX = 0x15,
REG_OOK_AVG = 0x16,
REG_AFC_FEI = 0x1A,
REG_PREAMBLE_DETECT = 0x1F,
REG_PREAMBLE_SIZE_MSB = 0x25,
REG_PREAMBLE_SIZE_LSB = 0x26,
REG_SYNC_CONFIG = 0x27,
REG_SYNC_VALUE1 = 0x28,
REG_SYNC_VALUE2 = 0x29,
REG_SYNC_VALUE3 = 0x2A,
REG_SYNC_VALUE4 = 0x2B,
REG_SYNC_VALUE5 = 0x2C,
REG_SYNC_VALUE6 = 0x2D,
REG_SYNC_VALUE7 = 0x2E,
REG_SYNC_VALUE8 = 0x2F,
REG_PACKET_CONFIG_1 = 0x30,
REG_PACKET_CONFIG_2 = 0x31,
REG_PAYLOAD_LENGTH_LSB = 0x32,
REG_FIFO_THRESH = 0x35,
REG_IMAGE_CAL = 0x3B,
// LoRa registers
REG_FIFO_ADDR_PTR = 0x0D,
REG_FIFO_TX_BASE_ADDR = 0x0E,
REG_FIFO_RX_BASE_ADDR = 0x0F,
REG_FIFO_RX_CURR_ADDR = 0x10,
REG_IRQ_FLAGS_MASK = 0x11,
REG_IRQ_FLAGS = 0x12,
REG_NB_RX_BYTES = 0x13,
REG_MODEM_STAT = 0x18,
REG_PKT_SNR_VALUE = 0x19,
REG_PKT_RSSI_VALUE = 0x1A,
REG_RSSI_VALUE = 0x1B,
REG_HOP_CHANNEL = 0x1C,
REG_MODEM_CONFIG1 = 0x1D,
REG_MODEM_CONFIG2 = 0x1E,
REG_SYMB_TIMEOUT_LSB = 0x1F,
REG_PREAMBLE_LEN_MSB = 0x20,
REG_PREAMBLE_LEN_LSB = 0x21,
REG_PAYLOAD_LENGTH = 0x22,
REG_HOP_PERIOD = 0x24,
REG_FIFO_RX_BYTE_ADDR = 0x25,
REG_MODEM_CONFIG3 = 0x26,
REG_FEI_MSB = 0x28,
REG_FEI_MIB = 0x29,
REG_FEI_LSB = 0x2A,
REG_DETECT_OPTIMIZE = 0x31,
REG_INVERT_IQ = 0x33,
REG_DETECT_THRESHOLD = 0x37,
REG_SYNC_WORD = 0x39,
};
enum SX127xOpMode : uint8_t {
MOD_LORA = 0x80,
ACCESS_FSK_REGS = 0x40,
ACCESS_LORA_REGS = 0x00,
MOD_OOK = 0x20,
MOD_FSK = 0x00,
ACCESS_LF_REGS = 0x08,
ACCESS_HF_REGS = 0x00,
MODE_CAD = 0x07,
MODE_RX_SINGLE = 0x06,
MODE_RX = 0x05,
MODE_RX_FS = 0x04,
MODE_TX = 0x03,
MODE_TX_FS = 0x02,
MODE_STDBY = 0x01,
MODE_SLEEP = 0x00,
MODE_MASK = 0x07,
};
enum SX127xPaConfig : uint8_t {
PA_PIN_BOOST = 0x80,
PA_PIN_RFO = 0x00,
PA_MAX_POWER = 0x70,
};
enum SX127xPaRamp : uint8_t {
CUTOFF_BR_X_2 = 0x40,
CUTOFF_BR_X_1 = 0x20,
GAUSSIAN_BT_0_3 = 0x60,
GAUSSIAN_BT_0_5 = 0x40,
GAUSSIAN_BT_1_0 = 0x20,
SHAPING_NONE = 0x00,
PA_RAMP_10 = 0x0F,
PA_RAMP_12 = 0x0E,
PA_RAMP_15 = 0x0D,
PA_RAMP_20 = 0x0C,
PA_RAMP_25 = 0x0B,
PA_RAMP_31 = 0x0A,
PA_RAMP_40 = 0x09,
PA_RAMP_50 = 0x08,
PA_RAMP_62 = 0x07,
PA_RAMP_100 = 0x06,
PA_RAMP_125 = 0x05,
PA_RAMP_250 = 0x04,
PA_RAMP_500 = 0x03,
PA_RAMP_1000 = 0x02,
PA_RAMP_2000 = 0x01,
PA_RAMP_3400 = 0x00,
};
enum SX127xDioMapping1 : uint8_t {
DIO0_MAPPING_00 = 0x00,
DIO0_MAPPING_01 = 0x40,
DIO0_MAPPING_10 = 0x80,
DIO0_MAPPING_11 = 0xC0,
};
enum SX127xRxConfig : uint8_t {
RESTART_ON_COLLISION = 0x80,
RESTART_NO_LOCK = 0x40,
RESTART_PLL_LOCK = 0x20,
AFC_AUTO_ON = 0x10,
AGC_AUTO_ON = 0x08,
TRIGGER_NONE = 0x00,
TRIGGER_RSSI = 0x01,
TRIGGER_PREAMBLE = 0x06,
TRIGGER_ALL = 0x07,
};
enum SX127xRxBw : uint8_t {
RX_BW_2_6 = 0x17,
RX_BW_3_1 = 0x0F,
RX_BW_3_9 = 0x07,
RX_BW_5_2 = 0x16,
RX_BW_6_3 = 0x0E,
RX_BW_7_8 = 0x06,
RX_BW_10_4 = 0x15,
RX_BW_12_5 = 0x0D,
RX_BW_15_6 = 0x05,
RX_BW_20_8 = 0x14,
RX_BW_25_0 = 0x0C,
RX_BW_31_3 = 0x04,
RX_BW_41_7 = 0x13,
RX_BW_50_0 = 0x0B,
RX_BW_62_5 = 0x03,
RX_BW_83_3 = 0x12,
RX_BW_100_0 = 0x0A,
RX_BW_125_0 = 0x02,
RX_BW_166_7 = 0x11,
RX_BW_200_0 = 0x09,
RX_BW_250_0 = 0x01,
};
enum SX127xOokPeak : uint8_t {
BIT_SYNC_ON = 0x20,
BIT_SYNC_OFF = 0x00,
OOK_THRESH_AVG = 0x10,
OOK_THRESH_PEAK = 0x08,
OOK_THRESH_FIXED = 0x00,
OOK_THRESH_STEP_6_0 = 0x07,
OOK_THRESH_STEP_5_0 = 0x06,
OOK_THRESH_STEP_4_0 = 0x05,
OOK_THRESH_STEP_3_0 = 0x04,
OOK_THRESH_STEP_2_0 = 0x03,
OOK_THRESH_STEP_1_5 = 0x02,
OOK_THRESH_STEP_1_0 = 0x01,
OOK_THRESH_STEP_0_5 = 0x00,
};
enum SX127xOokAvg : uint8_t {
OOK_THRESH_DEC_16 = 0xE0,
OOK_THRESH_DEC_8 = 0xC0,
OOK_THRESH_DEC_4 = 0xA0,
OOK_THRESH_DEC_2 = 0x80,
OOK_THRESH_DEC_1_8 = 0x60,
OOK_THRESH_DEC_1_4 = 0x40,
OOK_THRESH_DEC_1_2 = 0x20,
OOK_THRESH_DEC_1 = 0x00,
OOK_AVG_RESERVED = 0x10,
};
enum SX127xAfcFei : uint8_t {
AFC_AUTO_CLEAR_ON = 0x01,
};
enum SX127xPreambleDetect : uint8_t {
PREAMBLE_DETECTOR_ON = 0x80,
PREAMBLE_DETECTOR_OFF = 0x00,
PREAMBLE_DETECTOR_SIZE_SHIFT = 5,
PREAMBLE_DETECTOR_TOL_SHIFT = 0,
};
enum SX127xSyncConfig : uint8_t {
AUTO_RESTART_PLL_LOCK = 0x80,
AUTO_RESTART_NO_LOCK = 0x40,
AUTO_RESTART_OFF = 0x00,
PREAMBLE_55 = 0x20,
PREAMBLE_AA = 0x00,
SYNC_ON = 0x10,
SYNC_OFF = 0x00,
};
enum SX127xPacketConfig1 : uint8_t {
VARIABLE_LENGTH = 0x80,
FIXED_LENGTH = 0x00,
CRC_ON = 0x10,
CRC_OFF = 0x00,
};
enum SX127xPacketConfig2 : uint8_t {
CONTINUOUS_MODE = 0x00,
PACKET_MODE = 0x40,
};
enum SX127xFifoThresh : uint8_t {
TX_START_FIFO_EMPTY = 0x80,
TX_START_FIFO_LEVEL = 0x00,
};
enum SX127xImageCal : uint8_t {
AUTO_IMAGE_CAL_ON = 0x80,
IMAGE_CAL_START = 0x40,
IMAGE_CAL_RUNNING = 0x20,
TEMP_CHANGE = 0x08,
TEMP_THRESHOLD_20C = 0x06,
TEMP_THRESHOLD_15C = 0x04,
TEMP_THRESHOLD_10C = 0x02,
TEMP_THRESHOLD_5C = 0x00,
TEMP_MONITOR_OFF = 0x01,
TEMP_MONITOR_ON = 0x00,
};
enum SX127xIrqFlags : uint8_t {
RX_TIMEOUT = 0x80,
RX_DONE = 0x40,
PAYLOAD_CRC_ERROR = 0x20,
VALID_HEADER = 0x10,
TX_DONE = 0x08,
CAD_DONE = 0x04,
FHSS_CHANGE_CHANNEL = 0x02,
CAD_DETECTED = 0x01,
};
enum SX127xModemCfg1 : uint8_t {
BW_7_8 = 0x00,
BW_10_4 = 0x10,
BW_15_6 = 0x20,
BW_20_8 = 0x30,
BW_31_3 = 0x40,
BW_41_7 = 0x50,
BW_62_5 = 0x60,
BW_125_0 = 0x70,
BW_250_0 = 0x80,
BW_500_0 = 0x90,
CODING_RATE_4_5 = 0x02,
CODING_RATE_4_6 = 0x04,
CODING_RATE_4_7 = 0x06,
CODING_RATE_4_8 = 0x08,
IMPLICIT_HEADER = 0x01,
EXPLICIT_HEADER = 0x00,
};
enum SX127xModemCfg2 : uint8_t {
SPREADING_FACTOR_SHIFT = 4,
TX_CONTINOUS_MODE = 0x08,
RX_PAYLOAD_CRC_ON = 0x04,
RX_PAYLOAD_CRC_OFF = 0x00,
};
enum SX127xModemCfg3 : uint8_t {
LOW_DATA_RATE_OPTIMIZE_ON = 0x08,
MODEM_AGC_AUTO_ON = 0x04,
};
} // namespace sx127x
} // namespace esphome

View File

@ -87,6 +87,7 @@
#define USE_SELECT #define USE_SELECT
#define USE_SENSOR #define USE_SENSOR
#define USE_STATUS_LED #define USE_STATUS_LED
#define USE_STATUS_SENSOR
#define USE_SWITCH #define USE_SWITCH
#define USE_TEXT #define USE_TEXT
#define USE_TEXT_SENSOR #define USE_TEXT_SENSOR

View File

@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.9.0 esptool==4.9.0
click==8.1.7 click==8.1.7
esphome-dashboard==20250514.0 esphome-dashboard==20250514.0
aioesphomeapi==34.0.0 aioesphomeapi==34.1.0
zeroconf==0.147.0 zeroconf==0.147.0
puremagic==1.29 puremagic==1.29
ruamel.yaml==0.18.14 # dashboard_import ruamel.yaml==0.18.14 # dashboard_import

View File

@ -5,7 +5,8 @@ packages:
- !include package.yaml - !include package.yaml
- github://esphome/esphome/tests/components/template/common.yaml@dev - github://esphome/esphome/tests/components/template/common.yaml@dev
- url: https://github.com/esphome/esphome - url: https://github.com/esphome/esphome
file: tests/components/absolute_humidity/common.yaml path: tests/components/absolute_humidity
file: common.yaml
ref: dev ref: dev
refresh: 1d refresh: 1d

View File

@ -7,7 +7,8 @@ packages:
shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev
github: github:
url: https://github.com/esphome/esphome url: https://github.com/esphome/esphome
file: tests/components/absolute_humidity/common.yaml path: tests/components/absolute_humidity
file: common.yaml
ref: dev ref: dev
refresh: 1d refresh: 1d

View File

@ -36,5 +36,9 @@ binary_sensor:
- platform: packet_transport - platform: packet_transport
provider: unencrypted-device provider: unencrypted-device
id: other_binary_sensor_id id: other_binary_sensor_id
- platform: packet_transport
provider: some-device-name
type: status
name: Some-Device Status
- platform: template - platform: template
id: binary_sensor_id1 id: binary_sensor_id1

View File

@ -0,0 +1,45 @@
spi:
clk_pin: ${clk_pin}
mosi_pin: ${mosi_pin}
miso_pin: ${miso_pin}
sx127x:
cs_pin: ${cs_pin}
rst_pin: ${rst_pin}
dio0_pin: ${dio0_pin}
pa_pin: BOOST
pa_power: 17
pa_ramp: 40us
bitsync: true
bitrate: 4800
bandwidth: 50_0kHz
frequency: 433920000
modulation: FSK
deviation: 5000
rx_start: true
rx_floor: -90
packet_mode: true
payload_length: 8
sync_value: [0x33, 0x33]
shaping: NONE
preamble_size: 2
preamble_detect: 2
preamble_errors: 8
preamble_polarity: 0x55
on_packet:
then:
- sx127x.send_packet:
data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
button:
- platform: template
name: "SX127x Button"
on_press:
then:
- sx127x.set_mode_standby
- sx127x.run_image_cal
- sx127x.set_mode_tx
- sx127x.set_mode_sleep
- sx127x.set_mode_rx
- sx127x.send_packet:
data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]

View File

@ -0,0 +1,9 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO27
miso_pin: GPIO19
cs_pin: GPIO18
rst_pin: GPIO23
dio0_pin: GPIO26
<<: !include common.yaml

View File

@ -0,0 +1,9 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO18
miso_pin: GPIO19
cs_pin: GPIO1
rst_pin: GPIO2
dio0_pin: GPIO3
<<: !include common.yaml

View File

@ -0,0 +1,9 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO18
miso_pin: GPIO19
cs_pin: GPIO1
rst_pin: GPIO2
dio0_pin: GPIO3
<<: !include common.yaml

View File

@ -0,0 +1,9 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO27
miso_pin: GPIO19
cs_pin: GPIO18
rst_pin: GPIO23
dio0_pin: GPIO26
<<: !include common.yaml

View File

@ -0,0 +1,9 @@
substitutions:
clk_pin: GPIO5
mosi_pin: GPIO13
miso_pin: GPIO12
cs_pin: GPIO1
rst_pin: GPIO2
dio0_pin: GPIO3
<<: !include common.yaml

View File

@ -0,0 +1,9 @@
substitutions:
clk_pin: GPIO2
mosi_pin: GPIO3
miso_pin: GPIO4
cs_pin: GPIO5
rst_pin: GPIO6
dio0_pin: GPIO7
<<: !include common.yaml

View File

@ -0,0 +1,43 @@
esphome:
name: rapid-transitions-test
host:
api:
batch_delay: 0ms # Enable immediate sending for rapid transitions
logger:
level: DEBUG
# Add a sensor that updates frequently to trigger lambda evaluations
sensor:
- platform: template
name: "Update Trigger"
id: update_trigger
lambda: |-
return 0;
update_interval: 10ms
internal: true
# Simulate an IR remote binary sensor with rapid ON/OFF transitions
binary_sensor:
- platform: template
name: "Simulated IR Remote Button"
id: ir_remote_button
lambda: |-
// Simulate rapid button presses every ~100ms
// Each "press" is ON for ~30ms then OFF
uint32_t now = millis();
uint32_t press_cycle = now % 100; // 100ms cycle
// ON for first 30ms of each cycle
if (press_cycle < 30) {
// Only log state change
if (!id(ir_remote_button).state) {
ESP_LOGD("test", "Button ON at %u", now);
}
return true;
} else {
// Only log state change
if (id(ir_remote_button).state) {
ESP_LOGD("test", "Button OFF at %u", now);
}
return false;
}

View File

@ -0,0 +1,58 @@
"""Integration test for API batch_delay: 0 with rapid state transitions."""
from __future__ import annotations
import asyncio
import time
from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_batch_delay_zero_rapid_transitions(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that rapid binary sensor transitions are preserved with batch_delay: 0ms."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Track state changes
state_changes: list[tuple[bool, float]] = []
def on_state(state: EntityState) -> None:
"""Track state changes with timestamps."""
if isinstance(state, BinarySensorState):
state_changes.append((state.state, time.monotonic()))
# Subscribe to state changes
client.subscribe_states(on_state)
# Wait for entity info
entity_info, _ = await client.list_entities_services()
binary_sensors = [e for e in entity_info if isinstance(e, BinarySensorInfo)]
assert len(binary_sensors) == 1, "Expected 1 binary sensor"
# Collect states for 2 seconds
await asyncio.sleep(2.1)
# Count ON->OFF transitions
on_off_count = 0
for i in range(1, len(state_changes)):
if state_changes[i - 1][0] and not state_changes[i][0]: # ON to OFF
on_off_count += 1
# With batch_delay: 0, we should capture rapid transitions
# The test timing can be variable in CI, so we're being conservative
# We mainly want to verify that we capture multiple rapid transitions
assert on_off_count >= 5, (
f"Expected at least 5 ON->OFF transitions with batch_delay: 0ms, got {on_off_count}. "
"Rapid transitions may have been lost."
)
# Also verify that state changes are happening frequently
assert len(state_changes) >= 10, (
f"Expected at least 10 state changes, got {len(state_changes)}"
)

View File

@ -74,37 +74,41 @@ async def test_host_mode_empty_string_options(
# If we got here without protobuf decoding errors, the fix is working # If we got here without protobuf decoding errors, the fix is working
# The bug would have caused "Invalid protobuf message" errors with trailing bytes # The bug would have caused "Invalid protobuf message" errors with trailing bytes
# Also verify we can interact with the select entities # Also verify we can receive state updates for select entities
# Subscribe to state changes # This ensures empty strings work properly in state messages too
states: dict[int, EntityState] = {} states: dict[int, EntityState] = {}
state_change_future: asyncio.Future[None] = loop.create_future() states_received_future: asyncio.Future[None] = loop.create_future()
expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key}
received_select_keys = set()
def on_state(state: EntityState) -> None: def on_state(state: EntityState) -> None:
"""Track state changes.""" """Track state changes."""
states[state.key] = state states[state.key] = state
# When we receive the state change for our select, resolve the future # Track which select entities we've received states for
if state.key == empty_first.key and not state_change_future.done(): if state.key in expected_select_keys:
state_change_future.set_result(None) received_select_keys.add(state.key)
# Once we have all select states, we're done
if (
received_select_keys == expected_select_keys
and not states_received_future.done()
):
states_received_future.set_result(None)
client.subscribe_states(on_state) client.subscribe_states(on_state)
# Try setting a select to an empty string option # Wait for initial states with timeout
# This further tests that empty strings are handled correctly
client.select_command(empty_first.key, "")
# Wait for state update with timeout
try: try:
await asyncio.wait_for(state_change_future, timeout=5.0) await asyncio.wait_for(states_received_future, timeout=5.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pytest.fail( pytest.fail(
"Did not receive state update after setting select to empty string" f"Did not receive states for all select entities. "
f"Expected keys: {expected_select_keys}, Received: {received_select_keys}"
) )
# Verify the state was set to empty string # Verify we received states for all select entities
assert empty_first.key in states assert empty_first.key in states
select_state = states[empty_first.key] assert empty_middle.key in states
assert hasattr(select_state, "state") assert empty_last.key in states
assert select_state.state == ""
# The test passes if no protobuf decoding errors occurred # The main test is that we got here without protobuf errors
# With the bug, we would have gotten "Invalid protobuf message" errors # The select entities with empty string options were properly encoded

View File

@ -46,14 +46,22 @@ async def test_host_mode_fan_preset(
# Subscribe to states # Subscribe to states
states: dict[int, FanState] = {} states: dict[int, FanState] = {}
state_event = asyncio.Event() state_event = asyncio.Event()
initial_states_received = set()
def on_state(state: FanState) -> None: def on_state(state: FanState) -> None:
if isinstance(state, FanState): if isinstance(state, FanState):
states[state.key] = state states[state.key] = state
initial_states_received.add(state.key)
state_event.set() state_event.set()
client.subscribe_states(on_state) client.subscribe_states(on_state)
# Wait for initial states to be received for all fans
expected_fan_keys = {fan.key for fan in fans}
while initial_states_received != expected_fan_keys:
state_event.clear()
await asyncio.wait_for(state_event.wait(), timeout=2.0)
# Test 1: Turn on fan without speed or preset - should set speed to 100% # Test 1: Turn on fan without speed or preset - should set speed to 100%
state_event.clear() state_event.clear()
client.fan_command( client.fan_command(

View File

@ -22,36 +22,51 @@ async def test_host_mode_many_entities(
async with run_compiled(yaml_config), api_client_connected() as client: async with run_compiled(yaml_config), api_client_connected() as client:
# Subscribe to state changes # Subscribe to state changes
states: dict[int, EntityState] = {} states: dict[int, EntityState] = {}
entity_count_future: asyncio.Future[int] = loop.create_future() sensor_count_future: asyncio.Future[int] = loop.create_future()
def on_state(state: EntityState) -> None: def on_state(state: EntityState) -> None:
states[state.key] = state states[state.key] = state
# When we have received states from a good number of entities, resolve the future # Count sensor states specifically
if len(states) >= 50 and not entity_count_future.done(): sensor_states = [
entity_count_future.set_result(len(states)) s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, float)
]
# When we have received states from at least 50 sensors, resolve the future
if len(sensor_states) >= 50 and not sensor_count_future.done():
sensor_count_future.set_result(len(sensor_states))
client.subscribe_states(on_state) client.subscribe_states(on_state)
# Wait for states from at least 50 entities with timeout # Wait for states from at least 50 sensors with timeout
try: try:
entity_count = await asyncio.wait_for(entity_count_future, timeout=10.0) sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
sensor_states = [
s
for s in states.values()
if hasattr(s, "state") and isinstance(s.state, float)
]
pytest.fail( pytest.fail(
f"Did not receive states from at least 50 entities within 10 seconds. " f"Did not receive states from at least 50 sensors within 10 seconds. "
f"Received {len(states)} states: {list(states.keys())}" f"Received {len(sensor_states)} sensor states out of {len(states)} total states"
) )
# Verify we received a good number of entity states # Verify we received a good number of entity states
assert entity_count >= 50, f"Expected at least 50 entities, got {entity_count}" assert len(states) >= 50, (
assert len(states) >= 50, f"Expected at least 50 states, got {len(states)}" f"Expected at least 50 total states, got {len(states)}"
)
# Verify we have different entity types by checking some expected values # Verify we have the expected sensor states
sensor_states = [ sensor_states = [
s s
for s in states.values() for s in states.values()
if hasattr(s, "state") and isinstance(s.state, float) if hasattr(s, "state") and isinstance(s.state, float)
] ]
assert sensor_count >= 50, (
f"Expected at least 50 sensor states, got {sensor_count}"
)
assert len(sensor_states) >= 50, ( assert len(sensor_states) >= 50, (
f"Expected at least 50 sensor states, got {len(sensor_states)}" f"Expected at least 50 sensor states, got {len(sensor_states)}"
) )