Compare commits

..
Author SHA1 Message Date
J. Nick Koston d4ce8cc6b4 Merge remote-tracking branch 'upstream/dev' into noise-session-resume
# Conflicts:
#	esphome/components/noise/noise.h
2026-09-08 18:19:28 +02:00
J. Nick Koston 552fcebbba Merge remote-tracking branch 'upstream/dev' into noise-session-resume 2026-09-02 11:34:05 +02:00
J. Nick Koston e90b219e00 Merge remote-tracking branch 'upstream/dev' into noise-session-resume 2026-08-26 19:49:54 -05:00
J. Nick Koston cff5cf7ad5 [noise] Trim the resume header comments 2026-08-24 19:41:53 -05:00
J. Nick Koston a9b1fc361b [noise] Document the resume constraints, bound the KDF inputs, and test PSK rotation 2026-08-24 19:39:03 -05:00
J. Nick Koston b7a5056f3e Merge remote-tracking branch 'upstream/dev' into noise-session-resume 2026-08-24 17:37:39 -05:00
J. Nick Koston a19893b168 [noise] Inline the resume MAC and key helpers so try_accept calls the KDF directly 2026-08-24 16:55:30 -05:00
J. Nick Koston 2344929dae [noise] Keep the resume KDF labels in PROGMEM on ESP8266 2026-08-24 16:50:28 -05:00
J. Nick Koston 46de7335b2 [noise] Simplify the resume cache accept path and guard the sensitive message set 2026-08-24 16:44:47 -05:00
J. Nick Koston 1cb2136c38 [noise] Trim resume flash: one KDF, no discard state, packed ticket 2026-08-24 16:31:46 -05:00
J. Nick Koston d1f7e71efb [noise] Trim resume flash usage and never dump the ticket secret 2026-08-24 15:53:33 -05:00
J. Nick Koston cf97e4c4e8 [noise] Add a session resume integration test 2026-08-24 15:41:23 -05:00
J. Nick Koston 4e2ff94588 [noise] Keep two resume tickets 2026-08-24 14:39:15 -05:00
J. Nick Koston c2118778ee [noise] Move NoiseResumeTicket to message id 152 2026-08-24 14:31:36 -05:00
J. Nick Koston c655a2a442 [noise] Use NOLINT for the label memcpy 2026-08-24 14:31:36 -05:00
J. Nick Koston b8a79a26fb [noise] Trim comments 2026-08-24 14:31:35 -05:00
J. Nick Koston 34caf86839 [noise] Fix clang-tidy findings 2026-08-24 14:31:35 -05:00
J. Nick Koston 1eb4cff6b6 [noise] Simplify the resume implementation 2026-08-24 14:31:35 -05:00
J. Nick Koston b60950da76 [noise] Add known answer and cache tests for session resume 2026-08-24 14:31:35 -05:00
J. Nick Koston 7e6db4d335 [noise] Add session resume to the api noise transport 2026-08-24 14:31:35 -05:00
138 changed files with 1313 additions and 2755 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want
to keep it open. Also, please merge the latest dev branch into your
to keep it open. Also, please rebase your PR onto the latest dev
branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5
RUN \
platformio settings set enable_telemetry No \
+18 -33
View File
@@ -77,7 +77,6 @@ service APIConnection {
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
}
@@ -894,6 +893,20 @@ message NoiseEncryptionSetKeyResponse {
bool success = 1;
}
// Single-use session resume ticket, sent unsolicited by the device after a
// Noise connection authenticates. A client presents it in the ClientHello of
// its next connection to skip the curve25519 handshake; the device then
// issues a fresh ticket on that connection. Never sent on plaintext
// connections. Clients that do not understand it drop it silently.
// Contents are secret; the device generator redacts this message from dump_to
message NoiseResumeTicket {
option (id) = 152;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_NOISE";
bytes ticket = 1; // session_id(8) || secret(32)
}
// ==================== HOMEASSISTANT.SERVICE ====================
message SubscribeHomeassistantServicesRequest {
option (id) = 34;
@@ -2727,8 +2740,7 @@ enum SerialProxyParity {
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
@@ -2754,8 +2766,7 @@ message SerialProxyDataReceived {
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device. Only the subscribed client may write; writes from
// others are ignored (since API 1.17).
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
@@ -2766,8 +2777,7 @@ message SerialProxyWriteRequest {
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
// others are refused with PORT_IN_USE (since API 1.17).
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
@@ -2806,7 +2816,6 @@ enum SerialProxyRequestType {
// error the device answers with INVALID_ARGUMENT.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
}
enum SerialProxyStatus {
@@ -2819,8 +2828,7 @@ enum SerialProxyStatus {
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
}
// Generic request message for simple serial proxy operations. FLUSH requires an active
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
// Generic request message for simple serial proxy operations
message SerialProxyRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
@@ -2844,29 +2852,6 @@ message SerialProxyRequestResponse {
string error_message = 4; // Additional detail on failure (optional)
}
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
// activates the port's protocol-aware tap (if one is configured), letting it observe
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
// speaks is a property of the device configuration, discoverable from the tap
// component's own API surface. A client that is about to flash firmware selects RAW
// first, which definitively disables that injection.
enum SerialProxyMode {
SERIAL_PROXY_MODE_RAW = 0;
SERIAL_PROXY_MODE_PROTOCOL = 1;
}
// Only the subscribed client may change the mode; any other caller -- including one that
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
// when the port has no protocol-aware tap configured.
message SerialProxySetModeRequest {
option (id) = 152;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyMode mode = 2;
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
+25 -15
View File
@@ -1661,7 +1661,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
break;
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
// Response-only discriminators; never valid in a request
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
@@ -1674,19 +1673,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
send_serial_proxy_ack(this, msg.instance, msg.type, status);
}
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return;
}
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
serial_proxy_result_to_status(result));
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
if (!this->send_message(msg)) {
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
@@ -1793,6 +1779,9 @@ void APIConnection::complete_authentication_() {
this->send_time_request();
}
#endif
#ifdef USE_API_NOISE
this->send_resume_ticket_();
#endif
#ifdef USE_ZWAVE_PROXY
if (zwave_proxy::global_zwave_proxy != nullptr) {
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
@@ -1800,6 +1789,27 @@ void APIConnection::complete_authentication_() {
#endif
}
#ifdef USE_API_NOISE
void APIConnection::send_resume_ticket_() {
#ifdef USE_API_PLAINTEXT
// Only encrypted transports get a ticket: on dual-mode builds a plaintext
// connection has no frame footer
if (this->helper_->frame_footer_size() == 0) {
return;
}
#endif
noise::ResumeTicket ticket;
if (!this->parent_->get_noise_ctx().resume_cache().issue(ticket)) {
return;
}
NoiseResumeTicket msg;
msg.set_ticket(reinterpret_cast<const uint8_t *>(&ticket), sizeof(ticket));
// A dropped ticket is harmless: the client does a full handshake next time
static_cast<void>(this->send_message(msg));
noise_clean(&ticket, sizeof(ticket));
}
#endif
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
// Copy client name with truncation if needed (set_client_name handles truncation)
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
@@ -1813,7 +1823,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 17;
resp.api_version_minor = 16;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name());
+5 -1
View File
@@ -244,7 +244,6 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
@@ -382,6 +381,11 @@ class APIConnection final : public APIServerConnectionBase {
// Helper function to handle authentication completion
void complete_authentication_();
#ifdef USE_API_NOISE
// Issue a fresh single-use session resume ticket over the encrypted channel
void send_resume_ticket_();
#endif
// Pattern B helpers: send response and return success/failure
bool send_hello_response_(const HelloRequest &msg);
bool send_disconnect_response_();
@@ -271,8 +271,8 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
// Contents are extension flags (today: the resume offer); mixed into the
// prologue either way. Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
size_t rx_size = this->rx_buf_.size();
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
@@ -289,6 +289,8 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_server_hello_() {
// A verified resume offer (still in rx_buf_ from the client hello step)
// replaces the whole handshake; any failure falls back to the full one.
// send server hello
const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE];
@@ -302,7 +304,9 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
// + optional resume accept extension
constexpr size_t max_msg_size =
1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE + noise::RESUME_ACCEPT_SIZE;
uint8_t msg[max_msg_size];
// chosen proto
@@ -313,16 +317,32 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
// node mac, terminated by null byte
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
// The accept extension, if any, is written straight after the mac
size_t ext_len = this->ctx_.resume_cache().try_accept(
this->rx_buf_.data(), this->rx_buf_.size(), this->prologue_.data(), this->prologue_.size(), msg + total_size,
sizeof(msg) - total_size, send_cipher_, recv_cipher_);
bool resume = ext_len != 0;
total_size += ext_len;
APIError aerr = write_frame_(msg, total_size);
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
if (resume) {
// A resuming client waits for this hello instead of pipelining
// handshake message 1, so the transport is ready now
this->frame_footer_size_ = noise_cipherstate_get_mac_length(this->send_cipher_);
HELPER_LOG("Session resumed!");
state_ = State::DATA;
} else {
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
}
// init_handshake_ copied the prologue into the handshake state; the resume
// path is done with it too
this->prologue_.release();
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_handshake_() {
@@ -552,8 +572,6 @@ APIError APINoiseFrameHelper::init_handshake_() {
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// init copies the prologue into the handshakestate, so we can get rid of it now
prologue_.release();
return APIError::OK;
}
+10 -13
View File
@@ -1061,6 +1061,16 @@ uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const {
size += ProtoSize::calc_bool(1, this->success);
return size;
}
uint8_t *NoiseResumeTicket::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->ticket_ptr_, this->ticket_len_);
return pos;
}
uint32_t NoiseResumeTicket::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_length(1, this->ticket_len_);
return size;
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
uint8_t *HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -4253,19 +4263,6 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
size += ProtoSize::calc_length(1, this->error_message.size());
return size;
}
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
case 2:
this->mode = static_cast<enums::SerialProxyMode>(value);
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+21 -21
View File
@@ -356,7 +356,6 @@ enum SerialProxyRequestType : uint32_t {
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
};
enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_OK = 0,
@@ -367,10 +366,6 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
};
enum SerialProxyMode : uint32_t {
SERIAL_PROXY_MODE_RAW = 0,
SERIAL_PROXY_MODE_PROTOCOL = 1,
};
#endif
} // namespace enums
@@ -1152,6 +1147,27 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
protected:
};
class NoiseResumeTicket final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 152;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("noise_resume_ticket"); }
#endif
const uint8_t *ticket_ptr_{nullptr};
size_t ticket_len_{0};
void set_ticket(const uint8_t *data, size_t len) {
this->ticket_ptr_ = data;
this->ticket_len_ = len;
}
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
class HomeassistantServiceMap final : public ProtoMessage {
@@ -3408,22 +3424,6 @@ class SerialProxyRequestResponse final : public ProtoMessage {
protected:
};
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 152;
static constexpr uint8_t ESTIMATED_SIZE = 6;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
#endif
uint32_t instance{0};
enums::SerialProxyMode mode{};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
+4 -18
View File
@@ -854,8 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -880,16 +878,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
return ESPHOME_PSTR("UNKNOWN");
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
switch (value) {
case enums::SERIAL_PROXY_MODE_RAW:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
case enums::SERIAL_PROXY_MODE_PROTOCOL:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
@@ -1405,6 +1393,10 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("success"), this->success);
return out.c_str();
}
const char *NoiseResumeTicket::dump_to(DumpBuffer &out) const {
out.append_p(ESPHOME_PSTR("NoiseResumeTicket {}"));
return out.c_str();
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
@@ -2817,12 +2809,6 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
return out.c_str();
}
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -712,17 +712,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_device_capabilities_request();
break;
}
#ifdef USE_SERIAL_PROXY
case SerialProxySetModeRequest::MESSAGE_TYPE: {
SerialProxySetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
#endif
this->on_serial_proxy_set_mode_request(msg);
break;
}
#endif
default:
break;
}
-3
View File
@@ -235,9 +235,6 @@ class APIServerConnectionBase {
void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
@@ -58,9 +58,6 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &ou
if (current_audio_file_ != nullptr) {
// A transfer buffer isn't ncessary for a local file
this->file_ring_buffer_ = output_ring_buffer.lock();
if (this->file_ring_buffer_ == nullptr) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
@@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le
void AudioTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0;
if (this->ring_buffer_ != nullptr) {
if (this->ring_buffer_.use_count() > 0) {
this->ring_buffer_->reset();
}
}
void AudioSinkTransferBuffer::clear_buffered_data() {
this->buffer_length_ = 0;
if (this->ring_buffer_ != nullptr) {
if (this->ring_buffer_.use_count() > 0) {
this->ring_buffer_->reset();
}
#ifdef USE_SPEAKER
@@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() {
}
bool AudioTransferBuffer::has_buffered_data() const {
if (this->ring_buffer_ != nullptr) {
if (this->ring_buffer_.use_count() > 0) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
}
return (this->available() > 0);
@@ -144,7 +144,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_ != nullptr) {
if (this->ring_buffer_.use_count() > 0) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
}
@@ -161,7 +161,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
} else
#endif
if (this->ring_buffer_ != nullptr) {
if (this->ring_buffer_.use_count() > 0) {
bytes_written =
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
} else if (this->sink_callback_ != nullptr) {
@@ -186,7 +186,7 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
return (this->speaker_->has_buffered_data() || (this->available() > 0));
}
#endif
if (this->ring_buffer_ != nullptr) {
if (this->ring_buffer_.use_count() > 0) {
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
}
return (this->available() > 0);
+1 -2
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir, remote_base
from esphome.components import climate_ir
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
@@ -12,5 +12,4 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
async def to_code(config: ConfigType) -> None:
remote_base.request_protocol("coolix") # used from C++
await climate_ir.new_climate_ir(config)
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning();
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
}
this->defer_sleep_();
this->next_enter_deep_sleep_ = true;
return false;
}
}
@@ -17,7 +17,6 @@ void DeepSleepComponent::setup() {
void DeepSleepComponent::schedule_sleep_() {
this->next_enter_deep_sleep_ = false;
this->disable_loop();
const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -46,7 +45,7 @@ void DeepSleepComponent::loop() {
void DeepSleepComponent::begin_sleep(bool manual) {
if (this->prevent_ && !manual) {
this->defer_sleep_();
this->next_enter_deep_sleep_ = true;
return;
}
@@ -190,11 +190,6 @@ class DeepSleepComponent final : public Component {
void schedule_sleep_();
bool should_teardown_();
void defer_sleep_() {
this->next_enter_deep_sleep_ = true;
this->enable_loop();
}
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
@@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning();
ESP_LOGW(TAG, "Waiting for wakeup pin state change");
}
this->defer_sleep_();
this->next_enter_deep_sleep_ = true;
return false;
}
return true;
+2 -3
View File
@@ -153,14 +153,13 @@ bool ES7210::configure_mic_gain_() {
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
// Configure mic 3
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
// Configure mic 4
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
@@ -41,10 +41,7 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const {
#endif
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
// Milliseconds for data transfer. Covers the lwIP retransmit run seen in
// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits
// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000;
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
// Single-instance pointer — multi-port configs are rejected in final_validate.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
@@ -118,24 +118,21 @@ void I2SAudioSpeakerBase::loop() {
break;
}
// Still starting up or winding down from a previous run
if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) {
break;
}
if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
this->status_momentary_error("driver-failure", 1000);
break;
}
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) {
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
}
break;
case speaker::STATE_RUNNING: // Intentional fallthrough
@@ -221,8 +218,8 @@ size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t
}
bool I2SAudioSpeakerBase::has_buffered_data() const {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
if (this->audio_ring_buffer_.use_count() > 0) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
return temp_ring_buffer->available() > 0;
}
return false;
+5
View File
@@ -59,6 +59,11 @@ void Infrared::setup() {
// Set up traits based on configuration
this->traits_.set_supports_transmitter(this->has_transmitter());
this->traits_.set_supports_receiver(this->has_receiver());
// Register as listener for received IR data
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void Infrared::dump_config() {
@@ -1,9 +1,6 @@
"""IR/RF Proxy component - provides remote_base backend for infrared platform."""
import esphome.codegen as cg
from esphome.components import remote_base
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
@@ -12,10 +9,3 @@ ir_rf_proxy_ns = cg.esphome_ns.namespace("ir_rf_proxy")
CONF_REMOTE_RECEIVER_ID = "remote_receiver_id"
CONF_REMOTE_TRANSMITTER_ID = "remote_transmitter_id"
async def attach_receiver(var: MockObj, config: ConfigType) -> None:
"""Wire the configured remote_receiver to a proxy entity and register it as a listener."""
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
remote_base.add_listener(receiver, var)
+3 -7
View File
@@ -9,12 +9,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
from . import (
CONF_REMOTE_RECEIVER_ID,
CONF_REMOTE_TRANSMITTER_ID,
attach_receiver,
ir_rf_proxy_ns,
)
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["infrared"]
@@ -87,7 +82,8 @@ async def to_code(config: dict[str, Any]) -> None:
# Link receiver if specified
if CONF_REMOTE_RECEIVER_ID in config:
await attach_receiver(var, config)
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
# Set receiver demodulation frequency if specified (metadata only, no hardware effect)
if CONF_RECEIVER_FREQUENCY in config:
@@ -97,6 +97,10 @@ void RfProxy::setup() {
// remote_transmitter/receiver always uses OOK (on-off keying)
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void RfProxy::dump_config() {
@@ -7,12 +7,7 @@ from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import (
CONF_REMOTE_RECEIVER_ID,
CONF_REMOTE_TRANSMITTER_ID,
attach_receiver,
ir_rf_proxy_ns,
)
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["radio_frequency"]
@@ -71,4 +66,5 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_transmitter(transmitter))
if CONF_REMOTE_RECEIVER_ID in config:
await attach_receiver(var, config)
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
+2 -11
View File
@@ -3,7 +3,6 @@
#include "esphome/components/esp32/crash_handler.h"
#include <esp_log.h>
#include <esp_idf_version.h>
#include <driver/uart.h>
#include <soc/soc_caps.h>
@@ -17,10 +16,8 @@
#include <driver/usb_serial_jtag_vfs.h>
#endif
#endif
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
#include "esp_sleep.h"
#endif
#include "esp_idf_version.h"
#include "freertos/FreeRTOS.h"
#include <fcntl.h>
@@ -90,12 +87,6 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
// ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes).
const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1;
uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0);
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
// Always flush before going to light sleep. Could be disabled for devices
// without TOP_PD or if source_clk = UART_SCLK_RTC
esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART);
#endif
}
void Logger::pre_setup() {
@@ -129,7 +129,7 @@ void MicroWakeWord::setup() {
return;
}
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
if (this->ring_buffer_.use_count() > 1) {
// Producer-only write: never touches consumer state. If the buffer is full, ask the inference task
// to drain it - reset() is a consumer operation and must run on the inference task's thread.
// Disable partial writes so audio chunks are either fully accepted or rejected and handled below.
@@ -446,9 +446,9 @@ void MicroWakeWord::loop() {
xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STOPPING);
}
// Retries on a subsequent loop if the task is still running on the other core
if ((event_group_bits & EventGroupBits::TASK_STOPPED) && this->inference_task_.deallocate()) {
if ((event_group_bits & EventGroupBits::TASK_STOPPED)) {
ESP_LOGD(TAG, "Inference task is finished, freeing task resources");
this->inference_task_.deallocate();
xEventGroupClearBits(this->event_group_, ALL_BITS);
xQueueReset(this->detection_queue_);
this->set_state_(State::STOPPED);
@@ -48,7 +48,7 @@ class MicrophoneSource final {
template<typename F> void add_data_callback(F &&data_callback) {
this->mic_->add_data_callback([this, data_callback](const std::vector<uint8_t> &data) {
if (this->enabled_ || this->passive_) {
if (this->processed_samples_ == nullptr) {
if (this->processed_samples_.use_count() == 0) {
// Create vector if its unused
this->processed_samples_ = std::make_shared<std::vector<uint8_t>>();
}
+1 -2
View File
@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, remote_base, remote_transmitter, sensor, uart
from esphome.components import climate, remote_transmitter, sensor, uart
from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode
from esphome.components.remote_base import CONF_TRANSMITTER_ID
import esphome.config_validation as cv
@@ -280,7 +280,6 @@ async def to_code(config):
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
if CONF_TRANSMITTER_ID in config:
remote_base.request_protocol("midea") # ir_transmitter.h uses it from C++
cg.add_define("USE_REMOTE_TRANSMITTER")
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter_))
+1 -5
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir, remote_base
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_USE_FAHRENHEIT
from esphome.types import ConfigType
@@ -19,9 +19,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
async def to_code(config: ConfigType) -> None:
# midea_ir uses MideaProtocol from C++ and auto-loads coolix, whose coolix.cpp uses
# CoolixProtocol even when no coolix climate is configured
remote_base.request_protocol("midea")
remote_base.request_protocol("coolix")
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))
@@ -218,7 +218,7 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
}
size_t bytes_written = 0;
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
if (temp_ring_buffer.use_count() > 0) {
// Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
if (bytes_written > 0) {
@@ -250,14 +250,14 @@ esp_err_t SourceSpeaker::start_() {
// avoids unnecessary single-frame splices.
const size_t ring_buffer_size =
(this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
if (this->audio_source_ == nullptr) {
if (this->audio_source_.use_count() == 0) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer == nullptr) {
if (!temp_ring_buffer) {
temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
this->ring_buffer_ = temp_ring_buffer;
}
if (temp_ring_buffer == nullptr) {
if (!temp_ring_buffer) {
return ESP_ERR_NO_MEM;
}
@@ -278,7 +278,7 @@ void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
bool SourceSpeaker::has_buffered_data() const {
return ((this->audio_source_ != nullptr) && this->audio_source_->has_buffered_data());
return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data());
}
void SourceSpeaker::set_mute_state(bool mute_state) {
@@ -382,8 +382,8 @@ void MixerSpeaker::loop() {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
}
// Retries on a subsequent loop if the task is still running on the other core
if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) {
if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
this->task_.deallocate();
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
this->all_stopped_since_ms_ = 0;
@@ -496,7 +496,7 @@ void MixerSpeaker::audio_mixer_task(void *params) {
if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data
std::shared_ptr<audio::RingBufferAudioSource> audio_source = speaker->get_audio_source().lock();
if (audio_source == nullptr) {
if (audio_source.use_count() == 0) {
// No audio source allocated, so skip processing this speaker
continue;
}
+42 -135
View File
@@ -26,129 +26,44 @@ static const uint8_t MLX90614_ID4 = 0x3F;
static const char *const TAG = "mlx90614";
// The EEPROM cell has a limited number of write cycles, so stop retrying after a few failures
static constexpr uint8_t EMISSIVITY_WRITE_ATTEMPTS = 3;
// SMBus packet error code: CRC-8 with polynomial 0x07, MSB first
static uint8_t crc8_pec(const uint8_t *data, uint8_t len) { return crc8(data, len, 0x00, 0x07, true); }
void MLX90614Component::setup() {
if (std::isnan(this->emissivity_)) {
if (!this->write_emissivity_()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
this->emissivity_write_attempts_ = EMISSIVITY_WRITE_ATTEMPTS;
this->try_write_emissivity_();
if (this->emissivity_write_attempts_ != 0) {
this->status_set_warning(LOG_STR("Failed to write emissivity, will retry"));
}
}
void MLX90614Component::try_write_emissivity_() {
if (this->emissivity_write_attempts_ == 0) {
return;
}
if (this->write_emissivity_()) {
this->emissivity_write_attempts_ = 0;
return;
}
if (--this->emissivity_write_attempts_ == 0) {
ESP_LOGE(TAG, "Giving up on writing emissivity after %u attempts", EMISSIVITY_WRITE_ATTEMPTS);
this->emissivity_write_failed_ = true;
}
}
bool MLX90614Component::write_emissivity_() {
// Skip the write when the EEPROM already holds the desired value to save write cycles
uint16_t current_emissivity;
if (this->read_register_(MLX90614_EMISSIVITY, current_emissivity) != i2c::ERROR_OK) {
return false;
}
const auto desired_emissivity = static_cast<uint16_t>(this->emissivity_ * 0xFFFF);
if (current_emissivity == desired_emissivity) {
if (std::isnan(this->emissivity_))
return true;
}
return this->write_register_(MLX90614_EMISSIVITY, desired_emissivity);
}
bool MLX90614Component::write_register_(uint8_t reg, uint16_t data) {
// The PEC covers the whole write transaction: SLA+W, command, data low, data high
uint8_t buf[5];
buf[0] = this->address_ << 1;
buf[1] = reg;
// See datasheet 8.3.3.1 EEPROM write sequence
// 1. Write 0x0000 into the cell of interest (erases the cell)
buf[2] = buf[3] = 0;
buf[4] = crc8_pec(buf, 4);
auto ec = this->write_register(reg, buf + 2, 3);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Can't erase register 0x%02X, error %d", reg, ec);
uint16_t value = (uint16_t) (this->emissivity_ * 65535);
if (!this->write_bytes_(MLX90614_EMISSIVITY, 0)) {
return false;
}
// 2. Wait at least 5ms
delay(10);
// 3. Write the new value
if (data != 0) {
buf[2] = data & 0xFF;
buf[3] = data >> 8;
buf[4] = crc8_pec(buf, 4);
ec = this->write_register(reg, buf + 2, 3);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Can't write register 0x%02X, error %d", reg, ec);
return false;
}
// 4. Wait at least 5ms
delay(10);
}
// 5. Read back to confirm the value was stored
uint16_t read_back;
ec = this->read_register_(reg, read_back);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Can't check register 0x%02X value, error %d", reg, ec);
if (!this->write_bytes_(MLX90614_EMISSIVITY, value)) {
return false;
}
if (read_back != data) {
ESP_LOGW(TAG, "Read back mismatch on register 0x%02X. Expected 0x%04X, got 0x%04X", reg, data, read_back);
return false;
}
delay(10);
return true;
}
i2c::ErrorCode MLX90614Component::read_register_(uint8_t reg, uint16_t &data) {
// The PEC covers the whole read transaction: SLA+W, command, SLA+R, data low, data high
uint8_t buf[6];
bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) {
uint8_t buf[5];
buf[0] = this->address_ << 1;
buf[1] = reg;
buf[2] = (this->address_ << 1) | 0x01;
const auto ec = this->read_register(reg, buf + 3, 3);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "i2c read error %d", ec);
return ec;
}
const auto expected_pec = crc8_pec(buf, 5);
if (buf[5] != expected_pec) {
ESP_LOGW(TAG, "i2c CRC error. Expected 0x%02X, got 0x%02X", expected_pec, buf[5]);
return i2c::ERROR_CRC;
}
data = encode_uint16(buf[4], buf[3]);
return i2c::ERROR_OK;
buf[2] = data & 0xFF;
buf[3] = data >> 8;
buf[4] = crc8(buf, 4, 0x00, 0x07, true);
return this->write_bytes(reg, buf + 2, 3);
}
void MLX90614Component::dump_config() {
ESP_LOGCONFIG(TAG, "MLX90614:");
LOG_I2C_DEVICE(this);
if (this->emissivity_write_attempts_ != 0) {
ESP_LOGW(TAG, " Emissivity not written yet, will retry");
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Ambient", this->ambient_sensor_);
@@ -156,41 +71,33 @@ void MLX90614Component::dump_config() {
}
void MLX90614Component::update() {
// Temperature reads run regardless of the emissivity state so a failure still shows up as NAN
this->try_write_emissivity_();
// Publishes NAN on a bus or CRC failure so a stuck reading is visible instead of silently stale
auto publish_sensor = [this](sensor::Sensor *sensor, uint8_t reg) {
if (sensor == nullptr) {
return i2c::ERROR_OK;
}
uint16_t raw;
const auto ec = this->read_register_(reg, raw);
if (ec != i2c::ERROR_OK) {
sensor->publish_state(NAN);
return ec;
}
// Bit 15 set means the device flagged the reading as invalid
const float temperature = (raw & 0x8000) ? NAN : raw * 0.02f - 273.15f;
ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", sensor->get_name().c_str(), temperature);
sensor->publish_state(temperature);
return ec;
};
const auto object_ec = publish_sensor(this->object_sensor_, MLX90614_TEMPERATURE_OBJECT_1);
const auto ambient_ec = publish_sensor(this->ambient_sensor_, MLX90614_TEMPERATURE_AMBIENT);
if (object_ec != i2c::ERROR_OK || ambient_ec != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to read some sensors"));
} else if (this->emissivity_write_failed_) {
this->status_set_warning(LOG_STR("Failed to write emissivity"));
} else if (this->emissivity_write_attempts_ != 0) {
this->status_set_warning(LOG_STR("Failed to write emissivity, will retry"));
} else {
this->status_clear_warning();
uint8_t emissivity[3];
if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
uint8_t raw_object[3];
if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
uint8_t raw_ambient[3];
if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
float ambient = raw_ambient[1] & 0x80 ? NAN : encode_uint16(raw_ambient[1], raw_ambient[0]) * 0.02f - 273.15f;
float object = raw_object[1] & 0x80 ? NAN : encode_uint16(raw_object[1], raw_object[0]) * 0.02f - 273.15f;
ESP_LOGD(TAG, "Got Temperature=%.1f°C Ambient=%.1f°C", object, ambient);
if (this->ambient_sensor_ != nullptr && !std::isnan(ambient))
this->ambient_sensor_->publish_state(ambient);
if (this->object_sensor_ != nullptr && !std::isnan(object))
this->object_sensor_->publish_state(object);
this->status_clear_warning();
}
} // namespace esphome::mlx90614
+1 -6
View File
@@ -18,18 +18,13 @@ class MLX90614Component final : public PollingComponent, public i2c::I2CDevice {
void set_emissivity(float emissivity) { emissivity_ = emissivity; }
protected:
void try_write_emissivity_();
bool write_emissivity_();
bool write_register_(uint8_t reg, uint16_t data);
i2c::ErrorCode read_register_(uint8_t reg, uint16_t &data);
bool write_bytes_(uint8_t reg, uint16_t data);
sensor::Sensor *ambient_sensor_{nullptr};
sensor::Sensor *object_sensor_{nullptr};
float emissivity_{NAN};
// Remaining attempts to program the emissivity EEPROM cell, bounded to limit cell wear
uint8_t emissivity_write_attempts_{0};
bool emissivity_write_failed_{false};
};
} // namespace esphome::mlx90614
+6 -10
View File
@@ -26,34 +26,30 @@ namespace esphome::network {
/// Return whether the node is connected to the network (through wifi, eth, ...)
ESPHOME_ALWAYS_INLINE inline bool is_connected() {
// With a single interface enabled the checks below collapse to `if (x) return true; return false;`, which
// clang-tidy wants folded into one return. Keep the per-interface form so every enabled interface is checked.
// NOLINTBEGIN(readability-simplify-boolean-expr)
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
return true;
#endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr && modem::global_modem_component->is_connected())
return true;
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->is_connected();
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected())
return true;
if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_connected();
#endif
#ifdef USE_OPENTHREAD
if (openthread::global_openthread_component != nullptr && openthread::global_openthread_component->is_connected())
return true;
if (openthread::global_openthread_component != nullptr)
return openthread::global_openthread_component->is_connected();
#endif
#ifdef USE_HOST
return true; // Assume it's connected
#endif
return false;
// NOLINTEND(readability-simplify-boolean-expr)
}
/// Return whether the network is disabled: every configured interface with a
+2 -2
View File
@@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType:
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NOISE")
cg.add_library("esphome/noise-c", "0.1.26")
cg.add_library("esphome/noise-c", "0.1.24")
# noise-c depends on libsodium, but declaring it here too lets the
# library manager see the full set up front instead of discovering
# libsodium only after noise-c has downloaded, so the two can download
# in parallel. The version must match noise-c's library.json.
cg.add_library("esphome/libsodium", "1.10021.8")
cg.add_library("esphome/libsodium", "1.10021.6")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
+9 -1
View File
@@ -6,6 +6,8 @@
#include <cstdint>
#include "esphome/core/log.h"
#include "noise_resume.h"
namespace esphome::noise {
using psk_t = std::array<uint8_t, 32>;
@@ -26,13 +28,19 @@ class NoiseContext {
/// psk points at 32 bytes that outlive the context (PROGMEM or caller owned
/// RAM); nullptr means no key. Runtime callers map the all-zeros key to
/// nullptr themselves; validation keeps it out of yaml.
void set_psk(const uint8_t *psk) { this->psk_ = psk; }
void set_psk(const uint8_t *psk) {
this->psk_ = psk;
// Resume tickets were minted under the old key; forget them
this->resume_cache_.clear();
}
/// Copy the key out (flash-aware on ESP8266); all zeros when none is set.
void load_psk(psk_t &out) const;
bool has_psk() const { return this->psk_ != nullptr; }
ResumeTicketCache &resume_cache() { return this->resume_cache_; }
protected:
const uint8_t *psk_{nullptr};
ResumeTicketCache resume_cache_;
};
/// Convert a noise error code to a readable error
+127
View File
@@ -0,0 +1,127 @@
#include "noise_resume.h"
#ifdef USE_NOISE
#include <cstring>
#include <noise/protocol.h>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::noise {
const char RESUME_LABEL_OFFER[6] PROGMEM = "offer";
const char RESUME_LABEL_CONFIRM[8] PROGMEM = "confirm";
const char RESUME_LABEL_KEYS[5] PROGMEM = "keys";
bool resume_kdf(const uint8_t *secret, const char *label, size_t label_len, const uint8_t *a, size_t a_len,
const uint8_t *b, size_t b_len, const uint8_t *hash_in, size_t hash_in_len, uint8_t *out1,
size_t out1_len, uint8_t *out2) {
uint8_t data[RESUME_KDF_MAX_DATA];
uint8_t scratch[32];
size_t len = label_len + a_len + b_len;
progmem_memcpy(data, label, label_len);
std::memcpy(data + label_len, a, a_len);
std::memcpy(data + label_len + a_len, b, b_len);
NoiseHashState *hash = nullptr;
if (noise_hashstate_new_by_id(&hash, NOISE_HASH_SHA256) != NOISE_ERROR_NONE) {
return false;
}
int err = NOISE_ERROR_NONE;
if (hash_in != nullptr) {
err = noise_hashstate_hash_one(hash, hash_in, hash_in_len, data + len, 32);
len += 32;
}
if (err == NOISE_ERROR_NONE) {
err = noise_hashstate_hkdf(hash, secret, RESUME_SECRET_SIZE, data, len, out1, out1_len,
out2 != nullptr ? out2 : scratch, 32);
}
noise_hashstate_free(hash);
noise_clean(data, sizeof(data));
noise_clean(scratch, sizeof(scratch));
return err == NOISE_ERROR_NONE;
}
bool ResumeTicketCache::issue(ResumeTicket &out) {
if (!random_bytes(reinterpret_cast<uint8_t *>(&out), sizeof(out))) {
return false;
}
uint8_t slot = this->next_;
this->next_ = static_cast<uint8_t>((slot + 1) % SLOTS);
this->slots_[slot] = out;
this->used_mask_ |= static_cast<uint8_t>(1u << slot);
return true;
}
size_t ResumeTicketCache::try_accept(const uint8_t *offer, size_t offer_len, const uint8_t *prologue,
size_t prologue_len, uint8_t *out_ext, size_t out_capacity,
NoiseCipherState *&send_cipher, NoiseCipherState *&recv_cipher) {
if (offer_len != RESUME_OFFER_SIZE || offer[0] != RESUME_OFFER_VERSION || out_capacity < RESUME_ACCEPT_SIZE) {
return 0;
}
const uint8_t *session_id = offer + RESUME_OFFER_SESSION_ID_OFFSET;
const uint8_t *client_nonce = offer + RESUME_OFFER_NONCE_OFFSET;
ResumeTicket *ticket = nullptr;
for (uint8_t i = 0; i < SLOTS; i++) {
if ((this->used_mask_ & (1u << i)) &&
std::memcmp(this->slots_[i].session_id, session_id, RESUME_SESSION_ID_SIZE) == 0) {
ticket = &this->slots_[i];
this->used_mask_ &= static_cast<uint8_t>(~(1u << i));
break;
}
}
if (ticket == nullptr) {
return 0;
}
uint8_t expected[RESUME_MAC_SIZE];
bool ok = resume_compute_offer_mac(ticket->secret, session_id, client_nonce, expected) &&
noise_is_equal(expected, offer + RESUME_OFFER_MAC_OFFSET, RESUME_MAC_SIZE);
noise_clean(expected, sizeof(expected));
if (!ok) {
// Bad MAC: keep the ticket so a forger cannot burn it
this->used_mask_ |= static_cast<uint8_t>(1u << static_cast<uint8_t>(ticket - this->slots_));
return 0;
}
// The ticket is spent from here; any later failure falls back to the full
// handshake and the client gets a fresh one.
uint8_t *server_nonce = out_ext + 1;
uint8_t k_c2d[32];
uint8_t k_d2c[32];
out_ext[0] = RESUME_ACCEPT_VERSION;
ok = random_bytes(server_nonce, RESUME_NONCE_SIZE) &&
resume_compute_confirm_mac(ticket->secret, client_nonce, server_nonce, out_ext + 1 + RESUME_NONCE_SIZE) &&
resume_derive_keys(ticket->secret, client_nonce, server_nonce, prologue, prologue_len, k_c2d, k_d2c);
noise_clean(ticket, sizeof(*ticket));
if (ok) {
recv_cipher = resume_make_cipher(k_c2d);
send_cipher = resume_make_cipher(k_d2c);
ok = recv_cipher != nullptr && send_cipher != nullptr;
if (!ok) {
noise_cipherstate_free(recv_cipher);
noise_cipherstate_free(send_cipher);
recv_cipher = nullptr;
send_cipher = nullptr;
}
}
noise_clean(k_c2d, sizeof(k_c2d));
noise_clean(k_d2c, sizeof(k_d2c));
return ok ? RESUME_ACCEPT_SIZE : 0;
}
void ResumeTicketCache::clear() {
noise_clean(this->slots_, sizeof(this->slots_));
this->used_mask_ = 0;
}
NoiseCipherState *resume_make_cipher(const uint8_t *key) {
NoiseCipherState *cipher = nullptr;
if (noise_cipherstate_new_by_id(&cipher, NOISE_CIPHER_CHACHAPOLY) != NOISE_ERROR_NONE) {
return nullptr;
}
if (noise_cipherstate_init_key(cipher, key, 32) != NOISE_ERROR_NONE) {
noise_cipherstate_free(cipher);
return nullptr;
}
return cipher;
}
} // namespace esphome::noise
#endif // USE_NOISE
+132
View File
@@ -0,0 +1,132 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NOISE
#include <cstddef>
#include <cstdint>
// Forward declaration matching <noise/protocol/cipherstate.h>; keeps noise-c
// headers out of everything that includes noise.h.
extern "C" {
typedef struct NoiseCipherState_s NoiseCipherState; // NOLINT(modernize-use-using)
}
namespace esphome::noise {
/** Session resume for the noise transports.
*
* After a full handshake the responder issues a single-use ticket over the
* encrypted channel. A client presents it in its next ClientHello and both
* sides derive the transport keys with HKDF-SHA256 alone, skipping the two
* curve25519 operations. Old peers ignore the extension bytes on both
* sides, so every mismatch degrades to a normal full handshake.
*
* HKDF is the Noise construction (noise_hashstate_hkdf). Derivations:
* offer_mac = HKDF(secret, "offer" || session_id || client_nonce).out1[:16]
* confirm_mac = HKDF(secret, "confirm" || client_nonce || server_nonce).out1[:16]
* k_c2d, k_d2c = HKDF(secret, "keys" || client_nonce || server_nonce || SHA256(prologue))
*
* An offering client sends handshake message 1 only after a decline.
* Resumed sessions have no ephemeral DH; the ticket is wiped on use.
*/
static constexpr uint8_t RESUME_OFFER_VERSION = 0x01;
static constexpr uint8_t RESUME_ACCEPT_VERSION = 0x01;
static constexpr size_t RESUME_SESSION_ID_SIZE = 8;
static constexpr size_t RESUME_NONCE_SIZE = 16;
static constexpr size_t RESUME_MAC_SIZE = 16;
static constexpr size_t RESUME_SECRET_SIZE = 32;
// ClientHello body: version | session_id | client_nonce | offer_mac
static constexpr size_t RESUME_OFFER_SIZE = 1 + RESUME_SESSION_ID_SIZE + RESUME_NONCE_SIZE + RESUME_MAC_SIZE; // 41
static constexpr size_t RESUME_OFFER_SESSION_ID_OFFSET = 1;
static constexpr size_t RESUME_OFFER_NONCE_OFFSET = RESUME_OFFER_SESSION_ID_OFFSET + RESUME_SESSION_ID_SIZE;
static constexpr size_t RESUME_OFFER_MAC_OFFSET = RESUME_OFFER_NONCE_OFFSET + RESUME_NONCE_SIZE;
// ServerHello trailing extension: version | server_nonce | confirm_mac
static constexpr size_t RESUME_ACCEPT_SIZE = 1 + RESUME_NONCE_SIZE + RESUME_MAC_SIZE; // 33
struct ResumeTicket {
uint8_t session_id[RESUME_SESSION_ID_SIZE];
uint8_t secret[RESUME_SECRET_SIZE];
};
// Sent on the wire as one blob: session_id || secret
static_assert(sizeof(ResumeTicket) == RESUME_SESSION_ID_SIZE + RESUME_SECRET_SIZE, "ticket must be packed");
/// Fixed-slot RAM cache of single-use resume tickets. Lost on reboot by
/// design: clients fall back to a full handshake.
class ResumeTicketCache {
public:
/// Generate a fresh ticket into out and store it, evicting the oldest
/// slot. Returns false (and stores nothing) if the RNG fails.
bool issue(ResumeTicket &out);
/// Accept a resume offer: verify and consume the ticket (single use; a
/// forged MAC never burns one), build both transport ciphers, and write
/// the ServerHello accept extension into out_ext. Returns the extension
/// length, or 0 (nothing allocated) on any miss, failure, or when
/// out_capacity is too small. Secrets are wiped internally.
size_t try_accept(const uint8_t *offer, size_t offer_len, const uint8_t *prologue, size_t prologue_len,
uint8_t *out_ext, size_t out_capacity, NoiseCipherState *&send_cipher,
NoiseCipherState *&recv_cipher);
/// Forget every ticket (PSK change).
void clear();
// Round robin; more clients than slots thrash and fall back to full handshakes
static constexpr uint8_t SLOTS = 2;
static_assert(SLOTS <= 8, "used_mask_ is uint8_t");
protected:
ResumeTicket slots_[SLOTS];
uint8_t used_mask_{0};
uint8_t next_{0};
};
/// HKDF labels, PROGMEM on ESP8266.
extern const char RESUME_LABEL_OFFER[6];
extern const char RESUME_LABEL_CONFIRM[8];
extern const char RESUME_LABEL_KEYS[5];
// Largest KDF input: "keys" || client_nonce || server_nonce || SHA256(prologue)
static constexpr size_t RESUME_KDF_MAX_DATA =
sizeof(RESUME_LABEL_KEYS) - 1 + RESUME_NONCE_SIZE + RESUME_NONCE_SIZE + 32;
/// Noise-construction HKDF-SHA256 keyed with the ticket secret over
/// label || a || b [|| SHA256(hash_in)], at most RESUME_KDF_MAX_DATA. out2 == nullptr means MAC only.
bool resume_kdf(const uint8_t *secret, const char *label, size_t label_len, const uint8_t *a, size_t a_len,
const uint8_t *b, size_t b_len, const uint8_t *hash_in, size_t hash_in_len, uint8_t *out1,
size_t out1_len, uint8_t *out2);
/// offer_mac for the ClientHello resume offer (what a client computes and
/// try_accept checks).
inline bool resume_compute_offer_mac(const uint8_t *secret, const uint8_t *session_id, const uint8_t *client_nonce,
uint8_t *out_mac) {
static_assert(sizeof(RESUME_LABEL_OFFER) - 1 + RESUME_SESSION_ID_SIZE + RESUME_NONCE_SIZE <= RESUME_KDF_MAX_DATA,
"KDF buffer");
return resume_kdf(secret, RESUME_LABEL_OFFER, sizeof(RESUME_LABEL_OFFER) - 1, session_id, RESUME_SESSION_ID_SIZE,
client_nonce, RESUME_NONCE_SIZE, nullptr, 0, out_mac, RESUME_MAC_SIZE, nullptr);
}
/// confirm_mac for the ServerHello extension.
inline bool resume_compute_confirm_mac(const uint8_t *secret, const uint8_t *client_nonce, const uint8_t *server_nonce,
uint8_t *out_mac) {
static_assert(sizeof(RESUME_LABEL_CONFIRM) - 1 + RESUME_NONCE_SIZE + RESUME_NONCE_SIZE <= RESUME_KDF_MAX_DATA,
"KDF buffer");
return resume_kdf(secret, RESUME_LABEL_CONFIRM, sizeof(RESUME_LABEL_CONFIRM) - 1, client_nonce, RESUME_NONCE_SIZE,
server_nonce, RESUME_NONCE_SIZE, nullptr, 0, out_mac, RESUME_MAC_SIZE, nullptr);
}
/// Derive the transport keys. k_c2d encrypts client-to-device traffic,
/// k_d2c device-to-client.
inline bool resume_derive_keys(const uint8_t *secret, const uint8_t *client_nonce, const uint8_t *server_nonce,
const uint8_t *prologue, size_t prologue_len, uint8_t *k_c2d, uint8_t *k_d2c) {
static_assert(sizeof(RESUME_LABEL_KEYS) - 1 + RESUME_NONCE_SIZE + RESUME_NONCE_SIZE + 32 <= RESUME_KDF_MAX_DATA,
"KDF buffer");
return resume_kdf(secret, RESUME_LABEL_KEYS, sizeof(RESUME_LABEL_KEYS) - 1, client_nonce, RESUME_NONCE_SIZE,
server_nonce, RESUME_NONCE_SIZE, prologue, prologue_len, k_c2d, 32, k_d2c);
}
/// Build a ChaChaPoly cipher state keyed with key (32 bytes); nullptr on
/// failure. Nonce counter starts at 0, exactly like a post-split cipher.
NoiseCipherState *resume_make_cipher(const uint8_t *key);
} // namespace esphome::noise
#endif // USE_NOISE
+4 -81
View File
@@ -1,11 +1,6 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -45,9 +40,7 @@ from esphome.const import (
CONF_ZERO,
)
from esphome.core import ID, coroutine
from esphome.cpp_generator import MockObj
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.util import Registry, SimpleRegistry
AUTO_LOAD = ["binary_sensor"]
@@ -97,25 +90,9 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
)
# Listener and dumper lists are StaticVectors sized from these counts, so every
# registration must go through add_listener / add_dumper
_request_listener_slot = cg.slot_counter("REMOTE_BASE_LISTENER_COUNT")
_request_dumper_slot = cg.slot_counter("REMOTE_BASE_DUMPER_COUNT")
def add_listener(receiver: MockObj, listener: MockObj) -> None:
_request_listener_slot()
cg.add(receiver.register_listener(listener))
def add_dumper(receiver: MockObj, dumper: MockObj) -> None:
_request_dumper_slot()
cg.add(receiver.register_dumper(dumper))
async def register_listener(var: MockObj, config: ConfigType) -> None:
async def register_listener(var, config):
receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
add_listener(receiver, var)
cg.add(receiver.register_listener(var))
async def register_transmittable(var, config):
@@ -123,47 +100,8 @@ async def register_transmittable(var, config):
cg.add(var.set_transmitter(transmitter_))
# Registry names that share a protocol source file
def _protocol_stem(name: str) -> str:
if name.startswith("rc_switch"):
return "rc_switch"
if name == "canalsatld":
return "canalsat"
return name
def protocol_define(name: str) -> str:
return f"USE_REMOTE_PROTOCOL_{_protocol_stem(name).upper()}"
def request_protocol(name: str) -> None:
"""Keep a protocol's source file in the build; components using it from C++ must call this."""
cg.add_define(protocol_define(name))
_PROTOCOL_STEMS = sorted(
path.name.removesuffix("_protocol.cpp")
for path in Path(__file__).parent.glob("*_protocol.cpp")
)
# Only the protocol sources a configuration uses are compiled
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS}
)
def register_binary_sensor(
name: str, type: MockObj, schema: cv.Schema | dict
) -> Callable[[Callable[[MockObj, ConfigType], Any]], Callable]:
registerer = BINARY_SENSOR_REGISTRY.register(name, type, schema)
def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable:
async def new_func(var: MockObj, config: ConfigType) -> None:
request_protocol(name)
await coroutine(func)(var, config)
return registerer(new_func)
return decorator
def register_binary_sensor(name, type, schema):
return BINARY_SENSOR_REGISTRY.register(name, type, schema)
def register_trigger(name, type, data_type):
@@ -176,7 +114,6 @@ def register_trigger(name, type, data_type):
def decorator(func):
async def new_func(config):
request_protocol(name)
var = cg.new_Pvariable(config[CONF_TRIGGER_ID])
await coroutine(func)(var, config)
await automation.build_automation(var, [(data_type, "x")], config)
@@ -194,7 +131,6 @@ def register_dumper(name, type, schema=None):
def decorator(func):
async def new_func(config, dumper_id):
request_protocol(name)
var = cg.new_Pvariable(dumper_id)
await coroutine(func)(var, config)
return var
@@ -235,7 +171,6 @@ def register_action(name, type_, schema):
def decorator(func):
async def new_func(config, action_id, template_arg, args):
request_protocol(name)
var = cg.new_Pvariable(action_id, template_arg)
await register_transmittable(var, config)
if CONF_REPEAT in config:
@@ -275,21 +210,9 @@ TRIGGER_REGISTRY = SimpleRegistry()
DUMPER_REGISTRY = Registry()
def _dumper_key(item: Any) -> Any:
"""Registry key of a dump entry in either its string or its mapping form."""
if isinstance(item, dict) and len(item) == 1:
return next(iter(item))
return item
def validate_dumpers(value):
if isinstance(value, str) and value.lower() == "all":
return validate_dumpers(list(DUMPER_REGISTRY.keys()))
if isinstance(value, list):
# a dumper listed twice would register twice; the receiver holds one secondary dumper
keys = [_dumper_key(item) for item in value]
if all(isinstance(key, str) for key in keys):
value = list(dict(zip(keys, value, strict=True)).values())
return cv.validate_registry("dumper", DUMPER_REGISTRY)(value)
@@ -191,9 +191,9 @@ class ABBWelcomeData {
class ABBWelcomeProtocol : public RemoteProtocol<ABBWelcomeData> {
public:
void encode(RemoteTransmitData *dst, const ABBWelcomeData &src);
optional<ABBWelcomeData> decode(RemoteReceiveData src);
void dump(const ABBWelcomeData &data);
void encode(RemoteTransmitData *dst, const ABBWelcomeData &src) override;
optional<ABBWelcomeData> decode(RemoteReceiveData src) override;
void dump(const ABBWelcomeData &data) override;
protected:
void encode_byte_(RemoteTransmitData *dst, uint8_t data) const;
@@ -15,9 +15,9 @@ struct AEHAData {
class AEHAProtocol : public RemoteProtocol<AEHAData> {
public:
void encode(RemoteTransmitData *dst, const AEHAData &data);
optional<AEHAData> decode(RemoteReceiveData src);
void dump(const AEHAData &data);
void encode(RemoteTransmitData *dst, const AEHAData &data) override;
optional<AEHAData> decode(RemoteReceiveData src) override;
void dump(const AEHAData &data) override;
private:
std::string format_data_(const std::vector<uint8_t> &data);
@@ -16,9 +16,9 @@ struct Beo4Data {
class Beo4Protocol : public RemoteProtocol<Beo4Data> {
public:
void encode(RemoteTransmitData *dst, const Beo4Data &data);
optional<Beo4Data> decode(RemoteReceiveData src);
void dump(const Beo4Data &data);
void encode(RemoteTransmitData *dst, const Beo4Data &data) override;
optional<Beo4Data> decode(RemoteReceiveData src) override;
void dump(const Beo4Data &data) override;
};
DECLARE_REMOTE_PROTOCOL(Beo4)
@@ -13,9 +13,9 @@ struct BrennenstuhlData {
class BrennenstuhlProtocol : public RemoteProtocol<BrennenstuhlData> {
public:
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data);
optional<BrennenstuhlData> decode(RemoteReceiveData src);
void dump(const BrennenstuhlData &data);
void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override;
optional<BrennenstuhlData> decode(RemoteReceiveData src) override;
void dump(const BrennenstuhlData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Brennenstuhl)
@@ -21,9 +21,9 @@ struct ByronSXData {
class ByronSXProtocol : public RemoteProtocol<ByronSXData> {
public:
void encode(RemoteTransmitData *dst, const ByronSXData &data);
optional<ByronSXData> decode(RemoteReceiveData src);
void dump(const ByronSXData &data);
void encode(RemoteTransmitData *dst, const ByronSXData &data) override;
optional<ByronSXData> decode(RemoteReceiveData src) override;
void dump(const ByronSXData &data) override;
};
DECLARE_REMOTE_PROTOCOL(ByronSX)
@@ -19,9 +19,9 @@ struct CanalSatLDData : public CanalSatData {};
class CanalSatBaseProtocol : public RemoteProtocol<CanalSatData> {
public:
void encode(RemoteTransmitData *dst, const CanalSatData &data);
optional<CanalSatData> decode(RemoteReceiveData src);
void dump(const CanalSatData &data);
void encode(RemoteTransmitData *dst, const CanalSatData &data) override;
optional<CanalSatData> decode(RemoteReceiveData src) override;
void dump(const CanalSatData &data) override;
protected:
uint16_t frequency_;
@@ -21,9 +21,9 @@ struct CoolixData {
class CoolixProtocol : public RemoteProtocol<CoolixData> {
public:
void encode(RemoteTransmitData *dst, const CoolixData &data);
optional<CoolixData> decode(RemoteReceiveData data);
void dump(const CoolixData &data);
void encode(RemoteTransmitData *dst, const CoolixData &data) override;
optional<CoolixData> decode(RemoteReceiveData data) override;
void dump(const CoolixData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Coolix)
@@ -13,9 +13,9 @@ struct DishData {
class DishProtocol : public RemoteProtocol<DishData> {
public:
void encode(RemoteTransmitData *dst, const DishData &data);
optional<DishData> decode(RemoteReceiveData src);
void dump(const DishData &data);
void encode(RemoteTransmitData *dst, const DishData &data) override;
optional<DishData> decode(RemoteReceiveData src) override;
void dump(const DishData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Dish)
@@ -20,9 +20,9 @@ struct DooyaData {
class DooyaProtocol : public RemoteProtocol<DooyaData> {
public:
void encode(RemoteTransmitData *dst, const DooyaData &data);
optional<DooyaData> decode(RemoteReceiveData src);
void dump(const DooyaData &data);
void encode(RemoteTransmitData *dst, const DooyaData &data) override;
optional<DooyaData> decode(RemoteReceiveData src) override;
void dump(const DooyaData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Dooya)
@@ -19,9 +19,9 @@ struct DraytonData {
class DraytonProtocol : public RemoteProtocol<DraytonData> {
public:
void encode(RemoteTransmitData *dst, const DraytonData &data);
optional<DraytonData> decode(RemoteReceiveData src);
void dump(const DraytonData &data);
void encode(RemoteTransmitData *dst, const DraytonData &data) override;
optional<DraytonData> decode(RemoteReceiveData src) override;
void dump(const DraytonData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Drayton)
@@ -21,9 +21,9 @@ struct DysonData {
class DysonProtocol : public RemoteProtocol<DysonData> {
public:
void encode(RemoteTransmitData *dst, const DysonData &data);
optional<DysonData> decode(RemoteReceiveData src);
void dump(const DysonData &data);
void encode(RemoteTransmitData *dst, const DysonData &data) override;
optional<DysonData> decode(RemoteReceiveData src) override;
void dump(const DysonData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Dyson)
@@ -31,9 +31,9 @@ class GoboxProtocol : public RemoteProtocol<GoboxData> {
void dump_timings_(const RawTimings &timings) const;
public:
void encode(RemoteTransmitData *dst, const GoboxData &data);
optional<GoboxData> decode(RemoteReceiveData src);
void dump(const GoboxData &data);
void encode(RemoteTransmitData *dst, const GoboxData &data) override;
optional<GoboxData> decode(RemoteReceiveData src) override;
void dump(const GoboxData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Gobox)
@@ -13,9 +13,9 @@ struct HaierData {
class HaierProtocol : public RemoteProtocol<HaierData> {
public:
void encode(RemoteTransmitData *dst, const HaierData &data);
optional<HaierData> decode(RemoteReceiveData src);
void dump(const HaierData &data);
void encode(RemoteTransmitData *dst, const HaierData &data) override;
optional<HaierData> decode(RemoteReceiveData src) override;
void dump(const HaierData &data) override;
protected:
void encode_byte_(RemoteTransmitData *dst, uint8_t item);
@@ -14,9 +14,9 @@ struct JVCData {
class JVCProtocol : public RemoteProtocol<JVCData> {
public:
void encode(RemoteTransmitData *dst, const JVCData &data);
optional<JVCData> decode(RemoteReceiveData src);
void dump(const JVCData &data);
void encode(RemoteTransmitData *dst, const JVCData &data) override;
optional<JVCData> decode(RemoteReceiveData src) override;
void dump(const JVCData &data) override;
};
DECLARE_REMOTE_PROTOCOL(JVC)
@@ -24,9 +24,9 @@ struct KeeloqData {
class KeeloqProtocol : public RemoteProtocol<KeeloqData> {
public:
void encode(RemoteTransmitData *dst, const KeeloqData &data);
optional<KeeloqData> decode(RemoteReceiveData src);
void dump(const KeeloqData &data);
void encode(RemoteTransmitData *dst, const KeeloqData &data) override;
optional<KeeloqData> decode(RemoteReceiveData src) override;
void dump(const KeeloqData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Keeloq)
+3 -3
View File
@@ -16,9 +16,9 @@ struct LGData {
class LGProtocol : public RemoteProtocol<LGData> {
public:
void encode(RemoteTransmitData *dst, const LGData &data);
optional<LGData> decode(RemoteReceiveData src);
void dump(const LGData &data);
void encode(RemoteTransmitData *dst, const LGData &data) override;
optional<LGData> decode(RemoteReceiveData src) override;
void dump(const LGData &data) override;
};
DECLARE_REMOTE_PROTOCOL(LG)
@@ -27,9 +27,9 @@ struct MagiQuestData {
class MagiQuestProtocol : public RemoteProtocol<MagiQuestData> {
public:
void encode(RemoteTransmitData *dst, const MagiQuestData &data);
optional<MagiQuestData> decode(RemoteReceiveData src);
void dump(const MagiQuestData &data);
void encode(RemoteTransmitData *dst, const MagiQuestData &data) override;
optional<MagiQuestData> decode(RemoteReceiveData src) override;
void dump(const MagiQuestData &data) override;
};
DECLARE_REMOTE_PROTOCOL(MagiQuest)
@@ -67,9 +67,9 @@ class MideaData {
class MideaProtocol : public RemoteProtocol<MideaData> {
public:
void encode(RemoteTransmitData *dst, const MideaData &src);
optional<MideaData> decode(RemoteReceiveData src);
void dump(const MideaData &data);
void encode(RemoteTransmitData *dst, const MideaData &src) override;
optional<MideaData> decode(RemoteReceiveData src) override;
void dump(const MideaData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Midea)
@@ -13,9 +13,9 @@ struct MirageData {
class MirageProtocol : public RemoteProtocol<MirageData> {
public:
void encode(RemoteTransmitData *dst, const MirageData &data);
optional<MirageData> decode(RemoteReceiveData src);
void dump(const MirageData &data);
void encode(RemoteTransmitData *dst, const MirageData &data) override;
optional<MirageData> decode(RemoteReceiveData src) override;
void dump(const MirageData &data) override;
protected:
void encode_byte_(RemoteTransmitData *dst, uint8_t item);
@@ -14,9 +14,9 @@ struct NECData {
class NECProtocol : public RemoteProtocol<NECData> {
public:
void encode(RemoteTransmitData *dst, const NECData &data);
optional<NECData> decode(RemoteReceiveData src);
void dump(const NECData &data);
void encode(RemoteTransmitData *dst, const NECData &data) override;
optional<NECData> decode(RemoteReceiveData src) override;
void dump(const NECData &data) override;
};
DECLARE_REMOTE_PROTOCOL(NEC)
@@ -24,9 +24,9 @@ class NexaProtocol : public RemoteProtocol<NexaData> {
void zero(RemoteTransmitData *dst) const;
void sync(RemoteTransmitData *dst) const;
void encode(RemoteTransmitData *dst, const NexaData &data);
optional<NexaData> decode(RemoteReceiveData src);
void dump(const NexaData &data);
void encode(RemoteTransmitData *dst, const NexaData &data) override;
optional<NexaData> decode(RemoteReceiveData src) override;
void dump(const NexaData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Nexa)
@@ -16,9 +16,9 @@ struct PanasonicData {
class PanasonicProtocol : public RemoteProtocol<PanasonicData> {
public:
void encode(RemoteTransmitData *dst, const PanasonicData &data);
optional<PanasonicData> decode(RemoteReceiveData src);
void dump(const PanasonicData &data);
void encode(RemoteTransmitData *dst, const PanasonicData &data) override;
optional<PanasonicData> decode(RemoteReceiveData src) override;
void dump(const PanasonicData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Panasonic)
@@ -13,9 +13,9 @@ struct PioneerData {
class PioneerProtocol : public RemoteProtocol<PioneerData> {
public:
void encode(RemoteTransmitData *dst, const PioneerData &data);
optional<PioneerData> decode(RemoteReceiveData src);
void dump(const PioneerData &data);
void encode(RemoteTransmitData *dst, const PioneerData &data) override;
optional<PioneerData> decode(RemoteReceiveData src) override;
void dump(const PioneerData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Pioneer)
@@ -30,9 +30,9 @@ class ProntoProtocol : public RemoteProtocol<ProntoData> {
std::string compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase);
public:
void encode(RemoteTransmitData *dst, const ProntoData &data);
optional<ProntoData> decode(RemoteReceiveData src);
void dump(const ProntoData &data);
void encode(RemoteTransmitData *dst, const ProntoData &data) override;
optional<ProntoData> decode(RemoteReceiveData src) override;
void dump(const ProntoData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Pronto)
@@ -14,9 +14,9 @@ struct RC5Data {
class RC5Protocol : public RemoteProtocol<RC5Data> {
public:
void encode(RemoteTransmitData *dst, const RC5Data &data);
optional<RC5Data> decode(RemoteReceiveData src);
void dump(const RC5Data &data);
void encode(RemoteTransmitData *dst, const RC5Data &data) override;
optional<RC5Data> decode(RemoteReceiveData src) override;
void dump(const RC5Data &data) override;
};
DECLARE_REMOTE_PROTOCOL(RC5)
@@ -15,9 +15,9 @@ struct RC6Data {
class RC6Protocol : public RemoteProtocol<RC6Data> {
public:
void encode(RemoteTransmitData *dst, const RC6Data &data);
optional<RC6Data> decode(RemoteReceiveData src);
void dump(const RC6Data &data);
void encode(RemoteTransmitData *dst, const RC6Data &data) override;
optional<RC6Data> decode(RemoteReceiveData src) override;
void dump(const RC6Data &data) override;
};
DECLARE_REMOTE_PROTOCOL(RC6)
@@ -1,12 +1,30 @@
#include "rc_switch_protocol.h"
#include <iterator>
#include "esphome/core/log.h"
namespace esphome::remote_base {
static const char *const TAG = "remote.rc_switch";
const RCSwitchBase RC_SWITCH_PROTOCOLS[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false),
RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false),
RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false),
RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false),
RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false),
RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false),
RCSwitchBase(10350, 450, 450, 900, 900, 450, true),
RCSwitchBase(300, 9300, 150, 900, 900, 150, false),
RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)};
RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low,
uint32_t one_high, uint32_t one_low, bool inverted)
: sync_high_(sync_high),
sync_low_(sync_low),
zero_high_(zero_high),
zero_low_(zero_low),
one_high_(one_high),
one_low_(one_low),
inverted_(inverted) {}
void RCSwitchBase::one(RemoteTransmitData *dst) const {
if (!this->inverted_) {
dst->mark(this->one_high_);
@@ -115,11 +133,11 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *o
optional<RCSwitchData> RCSwitchBase::decode(RemoteReceiveData &src) const {
RCSwitchData out;
uint8_t out_nbits;
for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) {
for (uint8_t i = 1; i <= 8; i++) {
src.reset();
const RCSwitchBase *protocol = &RC_SWITCH_PROTOCOLS[i];
if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) {
out.protocol = static_cast<uint8_t>(i);
out.protocol = i;
return out;
}
}
@@ -228,7 +246,7 @@ bool RCSwitchRawReceiver::matches(RemoteReceiveData src) {
return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_);
}
bool RCSwitchDumper::dump(RemoteReceiveData src) {
for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) {
for (uint8_t i = 1; i <= 8; i++) {
src.reset();
uint64_t out_data;
uint8_t out_nbits;
@@ -239,7 +257,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) {
buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0';
buffer[out_nbits] = '\0';
ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", static_cast<unsigned>(i), buffer);
ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer);
// only send first decoded protocol
return true;
@@ -16,16 +16,9 @@ class RCSwitchBase {
public:
using ProtocolData = RCSwitchData;
constexpr RCSwitchBase() = default;
constexpr RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low,
uint32_t one_high, uint32_t one_low, bool inverted)
: sync_high_(sync_high),
sync_low_(sync_low),
zero_high_(zero_high),
zero_low_(zero_low),
one_high_(one_high),
one_low_(one_low),
inverted_(inverted) {}
RCSwitchBase() = default;
RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high,
uint32_t one_low, bool inverted);
void one(RemoteTransmitData *dst) const;
@@ -65,21 +58,10 @@ class RCSwitchBase {
uint32_t zero_low_{};
uint32_t one_high_{};
uint32_t one_low_{};
uint32_t inverted_{}; // bool widened so every field is a word: the table is read from flash
bool inverted_{};
};
// Constant-initialized and kept in flash on every platform; all fields are 32-bit so ESP8266 can read it in place
inline constexpr RCSwitchBase RC_SWITCH_PROTOCOLS[] PROGMEM = {
{0, 0, 0, 0, 0, 0, false},
{350, 10850, 350, 1050, 1050, 350, false},
{650, 6500, 650, 1300, 1300, 650, false},
{3000, 7100, 400, 1100, 900, 600, false},
{380, 2280, 380, 1140, 1140, 380, false},
{3000, 7000, 500, 1000, 1000, 500, false},
{10350, 450, 450, 900, 900, 450, true},
{300, 9300, 150, 900, 900, 150, false},
{250, 2500, 250, 1250, 250, 250, false},
};
extern const RCSwitchBase RC_SWITCH_PROTOCOLS[9];
uint64_t decode_binary_string(const std::string &data);
+11 -29
View File
@@ -99,47 +99,29 @@ bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) {
/* RemoteReceiverBase */
// Slots are counted at code generation; a registration from C++ setup() has none
#ifdef REMOTE_BASE_LISTENER_COUNT
void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) {
if (this->listeners_.size() == REMOTE_BASE_LISTENER_COUNT) {
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("listener"),
LOG_STR_LITERAL("listener"));
return;
}
this->listeners_.push_back(listener);
}
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) {
if (dumper->is_secondary()) {
this->secondary_dumper_ = dumper;
return;
this->secondary_dumpers_.push_back(dumper);
} else {
this->dumpers_.push_back(dumper);
}
if (this->dumpers_.size() == REMOTE_BASE_DUMPER_COUNT) {
ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("dumper"),
LOG_STR_LITERAL("dumper"));
return;
}
this->dumpers_.push_back(dumper);
}
#endif
void RemoteReceiverBase::call_listeners_dumpers_() {
#ifdef REMOTE_BASE_LISTENER_COUNT
void RemoteReceiverBase::call_listeners_() {
for (auto *listener : this->listeners_)
listener->on_receive(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
}
void RemoteReceiverBase::call_dumpers_() {
bool success = false;
for (auto *dumper : this->dumpers_) {
if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)))
success = true;
}
if (!success && this->secondary_dumper_ != nullptr)
this->secondary_dumper_->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
#endif
if (!success) {
for (auto *dumper : this->secondary_dumpers_)
dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_));
}
}
void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); }
+20 -50
View File
@@ -1,14 +1,12 @@
#pragma once
#include <concepts>
#include <utility>
#include <vector>
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome::remote_base {
@@ -143,22 +141,6 @@ class RemoteRMTChannel {
#endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
// Protocol shapes, checked where a protocol is used so a missing method fails at the use site
// instead of deep inside a template body. Receive-only protocols such as RCSwitchBase decode
// without encoding.
template<typename T>
concept RemoteProtocolDecoder = requires(T proto, RemoteReceiveData src) {
{ proto.decode(src) } -> std::same_as<optional<typename T::ProtocolData>>;
};
template<typename T>
concept RemoteProtocolDumper = RemoteProtocolDecoder<T> && requires(T proto, const typename T::ProtocolData &data) {
proto.dump(data);
};
template<typename T>
concept RemoteProtocolEncoder = requires(T proto, RemoteTransmitData *dst, const typename T::ProtocolData &data) {
proto.encode(dst, data);
};
class RemoteTransmitterBase : public RemoteComponentBase {
public:
RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
@@ -180,7 +162,7 @@ class RemoteTransmitterBase : public RemoteComponentBase {
this->temp_.reset();
return TransmitCall(this);
}
template<RemoteProtocolEncoder Protocol>
template<typename Protocol>
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
auto call = this->transmit();
Protocol().encode(call.get_data(), data);
@@ -212,37 +194,24 @@ class RemoteReceiverDumperBase {
class RemoteReceiverBase : public RemoteComponentBase {
public:
RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
// Slots are counted at code generation; without one the call fails at compile time with the same message
// the runtime check logs
#ifdef REMOTE_BASE_LISTENER_COUNT
void register_listener(RemoteReceiverListener *listener);
#else
template<typename T> void register_listener(T *) {
static_assert(sizeof(T) == 0, "No listener slot: register it from to_code() with remote_base.add_listener");
}
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
void register_listener(RemoteReceiverListener *listener) { this->listeners_.push_back(listener); }
void register_dumper(RemoteReceiverDumperBase *dumper);
#else
template<typename T> void register_dumper(T *) {
static_assert(sizeof(T) == 0, "No dumper slot: register it from to_code() with remote_base.add_dumper");
}
#endif
void set_tolerance(uint32_t tolerance, ToleranceMode tolerance_mode) {
this->tolerance_ = tolerance;
this->tolerance_mode_ = tolerance_mode;
}
protected:
void call_listeners_dumpers_();
void call_listeners_();
void call_dumpers_();
void call_listeners_dumpers_() {
this->call_listeners_();
this->call_dumpers_();
}
#ifdef REMOTE_BASE_LISTENER_COUNT
StaticVector<RemoteReceiverListener *, REMOTE_BASE_LISTENER_COUNT> listeners_;
#endif
#ifdef REMOTE_BASE_DUMPER_COUNT
StaticVector<RemoteReceiverDumperBase *, REMOTE_BASE_DUMPER_COUNT> dumpers_;
RemoteReceiverDumperBase *secondary_dumper_{nullptr}; // runs only when no primary dumper matched
#endif
std::vector<RemoteReceiverListener *> listeners_;
std::vector<RemoteReceiverDumperBase *> dumpers_;
std::vector<RemoteReceiverDumperBase *> secondary_dumpers_;
RawTimings temp_;
uint32_t tolerance_{25};
ToleranceMode tolerance_mode_{TOLERANCE_MODE_PERCENTAGE};
@@ -260,14 +229,15 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial
/* TEMPLATES */
// Protocols are used only through their concrete type (see the RemoteProtocol* concepts); encode/decode/dump
// stay non-virtual so unused ones link out
template<typename T> class RemoteProtocol {
public:
using ProtocolData = T;
virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0;
virtual optional<ProtocolData> decode(RemoteReceiveData src) = 0;
virtual void dump(const ProtocolData &data) = 0;
};
template<RemoteProtocolDecoder T> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
public:
RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {}
@@ -285,7 +255,7 @@ template<RemoteProtocolDecoder T> class RemoteReceiverBinarySensor : public Remo
T::ProtocolData data_;
};
template<RemoteProtocolDecoder T>
template<typename T>
class RemoteReceiverTrigger final : public Trigger<typename T::ProtocolData>, public RemoteReceiverListener {
protected:
bool on_receive(RemoteReceiveData src) override {
@@ -306,7 +276,7 @@ class RemoteTransmittable {
void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
protected:
template<RemoteProtocolEncoder Protocol>
template<typename Protocol>
void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
}
@@ -328,7 +298,7 @@ template<typename... Ts> class RemoteTransmitterActionBase : public RemoteTransm
virtual void encode(RemoteTransmitData *dst, Ts... x) = 0;
};
template<RemoteProtocolDumper T> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
template<typename T> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
public:
bool dump(RemoteReceiveData src) override {
auto proto = T();
@@ -12,9 +12,9 @@ struct RoombaData {
class RoombaProtocol : public RemoteProtocol<RoombaData> {
public:
void encode(RemoteTransmitData *dst, const RoombaData &data);
optional<RoombaData> decode(RemoteReceiveData src);
void dump(const RoombaData &data);
void encode(RemoteTransmitData *dst, const RoombaData &data) override;
optional<RoombaData> decode(RemoteReceiveData src) override;
void dump(const RoombaData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Roomba)
@@ -16,9 +16,9 @@ struct Samsung36Data {
class Samsung36Protocol : public RemoteProtocol<Samsung36Data> {
public:
void encode(RemoteTransmitData *dst, const Samsung36Data &data);
optional<Samsung36Data> decode(RemoteReceiveData src);
void dump(const Samsung36Data &data);
void encode(RemoteTransmitData *dst, const Samsung36Data &data) override;
optional<Samsung36Data> decode(RemoteReceiveData src) override;
void dump(const Samsung36Data &data) override;
};
DECLARE_REMOTE_PROTOCOL(Samsung36)
@@ -14,9 +14,9 @@ struct SamsungData {
class SamsungProtocol : public RemoteProtocol<SamsungData> {
public:
void encode(RemoteTransmitData *dst, const SamsungData &data);
optional<SamsungData> decode(RemoteReceiveData src);
void dump(const SamsungData &data);
void encode(RemoteTransmitData *dst, const SamsungData &data) override;
optional<SamsungData> decode(RemoteReceiveData src) override;
void dump(const SamsungData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Samsung)
@@ -16,9 +16,9 @@ struct SonyData {
class SonyProtocol : public RemoteProtocol<SonyData> {
public:
void encode(RemoteTransmitData *dst, const SonyData &data);
optional<SonyData> decode(RemoteReceiveData src);
void dump(const SonyData &data);
void encode(RemoteTransmitData *dst, const SonyData &data) override;
optional<SonyData> decode(RemoteReceiveData src) override;
void dump(const SonyData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Sony)
@@ -17,9 +17,9 @@ struct SymphonyData {
class SymphonyProtocol : public RemoteProtocol<SymphonyData> {
public:
void encode(RemoteTransmitData *dst, const SymphonyData &data);
optional<SymphonyData> decode(RemoteReceiveData src);
void dump(const SymphonyData &data);
void encode(RemoteTransmitData *dst, const SymphonyData &data) override;
optional<SymphonyData> decode(RemoteReceiveData src) override;
void dump(const SymphonyData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Symphony)
@@ -14,9 +14,9 @@ struct ToshibaAcData {
class ToshibaAcProtocol : public RemoteProtocol<ToshibaAcData> {
public:
void encode(RemoteTransmitData *dst, const ToshibaAcData &data);
optional<ToshibaAcData> decode(RemoteReceiveData src);
void dump(const ToshibaAcData &data);
void encode(RemoteTransmitData *dst, const ToshibaAcData &data) override;
optional<ToshibaAcData> decode(RemoteReceiveData src) override;
void dump(const ToshibaAcData &data) override;
};
DECLARE_REMOTE_PROTOCOL(ToshibaAc)
@@ -16,9 +16,9 @@ struct TotoData {
class TotoProtocol : public RemoteProtocol<TotoData> {
public:
void encode(RemoteTransmitData *dst, const TotoData &data);
optional<TotoData> decode(RemoteReceiveData src);
void dump(const TotoData &data);
void encode(RemoteTransmitData *dst, const TotoData &data) override;
optional<TotoData> decode(RemoteReceiveData src) override;
void dump(const TotoData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Toto)
@@ -221,11 +221,11 @@ async def to_code(config: ConfigType) -> None:
dumpers = await remote_base.build_dumpers(config[CONF_DUMP])
for dumper in dumpers:
remote_base.add_dumper(var, dumper)
cg.add(var.register_dumper(dumper))
triggers = await remote_base.build_triggers(config)
for trigger in triggers:
remote_base.add_listener(var, trigger)
cg.add(var.register_listener(trigger))
await cg.register_component(var, config)
cg.add(
@@ -153,8 +153,8 @@ void ResamplerSpeaker::loop() {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
}
// Retries on a subsequent loop if the task is still running on the other core
if ((event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) && this->task_.deallocate()) {
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
this->task_.deallocate();
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
}
@@ -235,7 +235,7 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
} else {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
if (temp_ring_buffer) {
// Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
} else {
@@ -299,7 +299,7 @@ bool ResamplerSpeaker::has_buffered_data() const {
bool has_ring_buffer_data = false;
if (this->requires_resampling_()) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer != nullptr) {
if (temp_ring_buffer) {
has_ring_buffer_data = (temp_ring_buffer->available() > 0);
}
}
@@ -342,7 +342,7 @@ void ResamplerSpeaker::resample_task(void *params) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(
this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (temp_ring_buffer == nullptr) {
if (!temp_ring_buffer) {
err = ESP_ERR_NO_MEM;
} else {
this_resampler->ring_buffer_ = temp_ring_buffer;
+7 -51
View File
@@ -6,17 +6,12 @@ from esphome.components import esp32, network, psram, socket, wifi
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ESPHOME,
CONF_FORMAT,
CONF_HEIGHT,
CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_PROJECT,
CONF_SAMPLE_RATE,
CONF_SOURCE,
CONF_TASK_STACK_IN_PSRAM,
CONF_VERSION,
CONF_WIDTH,
)
from esphome.core import CORE, ID
@@ -32,18 +27,9 @@ DOMAIN = "sendspin"
CONF_DISPLAY_OFFSET = "display_offset"
CONF_SENDSPIN_ID = "sendspin_id"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_MANUFACTURER = "manufacturer"
# An empty device information string would be sent to the server as an empty value rather than
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`.
DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127))
CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
CONF_FIXED_DELAY = "fixed_delay"
CONF_DECODE_MEMORY = "decode_memory"
CONF_CODECS = "codecs"
# Matches ARTWORK_MAX_SLOTS in sendspin-cpp.
MAX_ARTWORK_SLOTS = 4
@@ -58,20 +44,6 @@ CODEC_FORMAT_OPUS = SendspinCodecFormat.enum("OPUS")
CODEC_FORMAT_PCM = SendspinCodecFormat.enum("PCM")
CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED")
CODEC_FLAC = "flac"
CODEC_OPUS = "opus"
CODEC_PCM = "pcm"
CODECS = {
CODEC_FLAC: CODEC_FORMAT_FLAC,
CODEC_OPUS: CODEC_FORMAT_OPUS,
CODEC_PCM: CODEC_FORMAT_PCM,
}
# Opus only supports 48 kHz audio, so it is left out of the default list at other rates.
DEFAULT_CODECS = [CODEC_FLAC, CODEC_OPUS, CODEC_PCM]
OPUS_SAMPLE_RATE = 48000
SendspinImageFormat = sendspin_library_ns.enum("SendspinImageFormat", is_class=True)
IMAGE_FORMAT_JPEG = SendspinImageFormat.enum("JPEG")
IMAGE_FORMAT_PNG = SendspinImageFormat.enum("PNG")
@@ -211,9 +183,6 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(SendspinHub),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
}
),
cv.only_on_esp32,
@@ -264,22 +233,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack()
# Device information for the server's client/hello message. Falls back to the project
# information, which is written as `manufacturer.model`. Anything still unset keeps the
# default the hub itself applies: the ESPHome name and version.
project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {})
project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".")
for value, setter in (
(config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer),
(config.get(CONF_MODEL) or project_model, var.set_model),
(
config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION),
var.set_firmware_version,
),
):
if value:
cg.add(setter(value))
# sendspin-cpp library
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
@@ -333,13 +286,16 @@ async def to_code(config: ConfigType) -> None:
if data.player_support:
cg.add_define("USE_SENDSPIN_PLAYER", True)
# Configures the player role. Each configured codec is advertised for 16 bits per sample
# mono and stereo at the configured sample rate. The order is a preference order, both for
# the codecs themselves and for stereo over mono.
# Configures the player role. We always assume support for 16 bits per sample mono and stereo FLAC, Opus, and PCM at the configured sample rate
# (with Opus only supported at 48 kHz since that's the only sample rate it supports). Users can configure the specific formats via the Sendspin server
player_cfg = data.player_config
sample_rate = player_cfg[CONF_SAMPLE_RATE]
codecs = [CODECS[codec] for codec in player_cfg[CONF_CODECS]]
# OPUS only supports 48 kHz audio
codecs = [CODEC_FORMAT_FLAC]
if sample_rate == 48000:
codecs.append(CODEC_FORMAT_OPUS)
codecs.append(CODEC_FORMAT_PCM)
def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer:
return cg.StructInitializer(
@@ -13,16 +13,11 @@ from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .. import (
CODEC_OPUS,
CODECS,
CONF_CODECS,
CONF_DECODE_MEMORY,
CONF_FIXED_DELAY,
CONF_INITIAL_STATIC_DELAY,
CONF_SENDSPIN_ID,
DEFAULT_CODECS,
MEMORY_LOCATIONS,
OPUS_SAMPLE_RATE,
SendspinHub,
register_player_config,
request_controller_support,
@@ -54,32 +49,10 @@ DisableStaticDelayAdjustmentAction = sendspin_ns.class_(
)
def _resolve_codecs(config: ConfigType) -> ConfigType:
"""Validate the codec preference list, filling in the default when it is not set."""
sample_rate = config[CONF_SAMPLE_RATE]
if (codecs := config.get(CONF_CODECS)) is None:
config[CONF_CODECS] = [
codec
for codec in DEFAULT_CODECS
if codec != CODEC_OPUS or sample_rate == OPUS_SAMPLE_RATE
]
return config
if len(set(codecs)) != len(codecs):
raise cv.Invalid("Each codec may only be listed once", path=[CONF_CODECS])
if CODEC_OPUS in codecs and sample_rate != OPUS_SAMPLE_RATE:
raise cv.Invalid(
f"Codec '{CODEC_OPUS}' requires a {CONF_SAMPLE_RATE} of {OPUS_SAMPLE_RATE}",
path=[CONF_CODECS],
)
return config
def _register(config: ConfigType) -> ConfigType:
request_controller_support()
register_player_config(
{
CONF_CODECS: config[CONF_CODECS],
CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE],
CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE],
CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY],
@@ -112,13 +85,9 @@ CONFIG_SCHEMA = cv.All(
min=16000, max=96000
),
cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True),
cv.Optional(CONF_CODECS): cv.All(
cv.ensure_list(cv.enum(CODECS, lower=True)), cv.Length(min=1)
),
}
),
cv.only_on_esp32,
_resolve_codecs,
_register,
)
+4 -12
View File
@@ -76,12 +76,8 @@ void SendspinHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Sendspin Hub:\n"
" Client ID: %s\n"
" Manufacturer: %s\n"
" Model: %s\n"
" Firmware version: %s\n"
" Task stack in PSRAM: %s",
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
this->firmware_version_, YESNO(this->task_stack_in_psram_));
get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
#ifdef USE_SENDSPIN_ARTWORK
// Slot indices come from the order the image platform entries were declared, so the log is the
@@ -131,19 +127,15 @@ const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_P
return get_mac_address_pretty_into_buffer(buf);
}
const char *SendspinHub::get_product_name_() const {
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
}
sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
sendspin::SendspinClientConfig config;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
config.name = App.get_friendly_name();
config.product_name = this->get_product_name_();
config.manufacturer = this->manufacturer_;
config.software_version = this->firmware_version_;
config.product_name = App.get_name();
config.manufacturer = "ESPHome";
config.software_version = ESPHOME_VERSION;
config.httpd_psram_stack = this->task_stack_in_psram_;
return config;
@@ -8,7 +8,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/version.h"
#include <sendspin/client.h>
#include <sendspin/config.h>
@@ -126,15 +125,6 @@ class SendspinHub final : public Component,
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
/// @brief Sets the device information reported to the server in the `client/hello` message.
///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
/// lifetime of the hub. Only called for values the configuration overrides; anything left alone
/// keeps the default described on the member below.
void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; }
void set_model(const char *model) { this->model_ = model; }
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
// --- Sendspin role specific methods ---
#ifdef USE_SENDSPIN_ARTWORK
@@ -197,9 +187,6 @@ class SendspinHub final : public Component,
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
sendspin::SendspinClientConfig build_client_config_();
/// @brief Returns the product name reported to the server: the configured model, or the device name.
const char *get_product_name_() const;
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
@@ -281,12 +268,6 @@ class SendspinHub final : public Component,
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
bool task_stack_in_psram_{false};
// Device information sent in the `client/hello` message. Defaults apply when neither the
// sendspin configuration nor the project information supplies a value.
const char *manufacturer_{"ESPHome"};
const char *model_{nullptr}; // nullptr reports the device name instead
const char *firmware_version_{ESPHOME_VERSION};
};
/// @brief Base class for all sendspin subcomponents.
@@ -30,7 +30,6 @@ MULTI_CONF = True
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
+25 -163
View File
@@ -29,57 +29,26 @@ void SerialProxy::setup() {
#ifdef USE_API
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
this->outgoing_msg_.instance = this->instance_index_;
#endif
#ifdef USE_SERIAL_PROXY_TAP
// A tap sets itself up before this runs (its setup priority is higher), so it may
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
// the loop enabled is what lets that finish; without it the tap would stall until a
// client happened to subscribe.
if (this->tap_ != nullptr && this->tap_->tap_needs_port()) {
return;
}
#endif
// No subscriber at startup; disable loop until a client subscribes
this->disable_loop();
}
#ifdef USE_SERIAL_PROXY_TAP
void SerialProxy::reset_mode_() {
// The mode belongs to a session, not to the port. Carrying a departed client's choice
// over to the next one would inject protocol bytes into a stream that never asked for
// them -- a firmware upload, or any client built before this request existed and so
// unable to turn it off. Guessing RAW is the safe direction: a client that wanted
// protocol handling and did not ask for it merely sends its own acknowledgements.
if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) {
return;
}
ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_);
this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW;
}
#endif
void SerialProxy::loop() {
#ifdef USE_API
// Detect subscriber disconnect
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->reset_mode_();
}
// With no subscriber there is normally nothing to do, but a tap may still need the port
// read -- it does its protocol work precisely while nobody else is listening.
// Safety check — loop should only run when subscribed, but guard against races
if (this->api_connection_ == nullptr) [[unlikely]] {
#ifdef USE_SERIAL_PROXY_TAP
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
return;
}
#else
this->disable_loop();
return;
#endif
}
// Detect subscriber disconnect
if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
!api_is_connected()) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->disable_loop();
return;
}
// Read available data from UART and forward to subscribed client
@@ -100,54 +69,11 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
if (!this->read_array(buffer, to_read))
return;
#ifdef USE_SERIAL_PROXY_TAP
// Before forwarding, so a tap that answers the device (an acknowledgement, say) is not
// waiting on the network round trip to a subscriber that may not even exist.
if (this->tap_observing_()) {
this->tap_->on_device_rx(buffer, to_read);
}
#endif
if (this->api_connection_ == nullptr) {
return;
}
this->outgoing_msg_.set_data(buffer, to_read);
this->api_connection_->send_serial_proxy_data(this->outgoing_msg_);
}
#endif
#ifdef USE_SERIAL_PROXY_TAP
bool SerialProxy::tap_observing_() const {
if (this->tap_ == nullptr) {
return false;
}
// With no subscriber, a tap doing its own protocol work (the boot-time handshake with
// the device, say) is served regardless of mode -- nobody has chosen one yet. Once a
// subscriber holds the port, the mode alone decides, so RAW stays inert.
if (this->api_connection_ == nullptr && this->tap_->tap_needs_port()) {
return true;
}
// Otherwise the mode decides. RAW must be inert: a client that flips to RAW before
// flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes
// into it, and "the tap turned out not to recognise the stream" is not good enough.
return this->mode_ == api::enums::SERIAL_PROXY_MODE_PROTOCOL;
}
void SerialProxy::tap_pump() {
#ifdef USE_API
// Nothing would consume the bytes; leave them in the FIFO
if (!this->tap_observing_() && this->api_connection_ == nullptr) {
return;
}
const size_t available = this->available();
if (available > 0) {
this->read_and_send_(available);
}
#endif
}
#endif
void SerialProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"Serial Proxy [%" PRIu32 "]:\n"
@@ -166,9 +92,8 @@ void SerialProxy::dump_config() {
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
#ifdef USE_API
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]",
this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -234,80 +159,24 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_connection,
api::enums::SerialProxyMode mode) {
#ifdef USE_API
// Only the live subscriber may change the mode, so the mode cannot outlive a session
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring mode request from client without port subscription [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
// Values come from a remote client
if (mode != api::enums::SERIAL_PROXY_MODE_RAW && mode != api::enums::SERIAL_PROXY_MODE_PROTOCOL) {
ESP_LOGW(TAG, "Invalid mode: %" PRIu32, static_cast<uint32_t>(mode));
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
}
// PROTOCOL on a port with no tap would be a silent no-op; refuse so the client knows
#ifdef USE_SERIAL_PROXY_TAP
const bool has_tap = this->tap_ != nullptr;
#else
const bool has_tap = false;
#endif
if (mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL && !has_tap) {
ESP_LOGW(TAG, "No tap on serial proxy [%" PRIu32 "]; PROTOCOL mode unavailable", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW"));
#ifdef USE_SERIAL_PROXY_TAP
const bool leaving_protocol_mode =
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
this->mode_ = mode;
// Only for an explicit client request, not for reset_mode_() at the end of a session:
// an ordinary disconnect says nothing about the device, whereas a client deliberately
// asking for raw bytes usually precedes changing what the device is.
if (leaving_protocol_mode && this->tap_ != nullptr) {
this->tap_->on_protocol_disabled();
}
#endif
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
#ifdef USE_API
// Bytes from anyone but the live subscriber would interleave with the subscriber's
// traffic -- or with an active tap's -- on the wire
if (!this->is_subscriber_(api_connection)) {
if (this->api_connection_ != nullptr) {
ESP_LOGW(TAG, "Ignoring write from client that does not hold serial proxy [%" PRIu32 "]", this->instance_index_);
} else {
// A legacy client streaming writes without subscribing would flood WARN, one per
// request; writes are the only high-rate, unacknowledged operation, so keep this
// visible without drowning the log
ESP_LOGV(TAG, "Ignoring write from client without port subscription [%" PRIu32 "]", this->instance_index_);
}
// Bytes from a client other than the live subscriber would interleave with the
// subscriber's traffic on the wire
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_);
return;
}
#endif
if (data == nullptr || len == 0)
return;
this->write_array(data, len);
#ifdef USE_SERIAL_PROXY_TAP
// After the write, so the tap observes the same ordering the device does
if (this->tap_observing_()) {
this->tap_->on_client_tx(data, len);
}
#endif
}
SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
#ifdef USE_API
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]",
this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -341,8 +210,8 @@ uint32_t SerialProxy::get_modem_pins() const {
SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
#ifdef USE_API
// Flushing stalls the port, so it gets the same ownership check as writes
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -361,6 +230,11 @@ SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
}
#ifdef USE_API
bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const {
return this->api_connection_ != nullptr && this->api_connection_ != api_connection &&
this->api_connection_->is_connection_setup();
}
SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection,
api::enums::SerialProxyRequestType type) {
switch (type) {
@@ -378,10 +252,6 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
// End the dead client's session before starting the new one, so its mode
// cannot leak into a session that never asked for it
this->api_connection_ = nullptr;
this->reset_mode_();
}
this->api_connection_ = api_connection;
this->enable_loop();
@@ -394,15 +264,7 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
this->api_connection_ = nullptr;
this->reset_mode_();
#ifdef USE_SERIAL_PROXY_TAP
// Keep the loop alive for a tap that still needs the port (mirrors loop())
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
}
#else
this->disable_loop();
#endif
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
default:
+3 -100
View File
@@ -26,7 +26,6 @@ class APIConnection;
namespace enums {
enum SerialProxyPortType : uint32_t;
enum SerialProxyRequestType : uint32_t;
enum SerialProxyMode : uint32_t;
} // namespace enums
} // namespace esphome::api
@@ -53,36 +52,6 @@ enum class SerialProxyResult : uint8_t {
/// Maximum bytes to read from UART in a single loop iteration
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
#ifdef USE_SERIAL_PROXY_TAP
/// Observes a port's traffic without owning it, and may inject bytes of its own.
///
/// This exists so protocol-aware behaviour can be layered onto a plain byte pipe without
/// the pipe knowing anything about the protocol: the tap is compiled in only when some
/// component asks for one, so a proxy carrying an RS485 meter pays nothing for it.
///
/// A tap is an observer, never a gatekeeper -- it cannot suppress or alter the bytes
/// flowing in either direction, so a misbehaving tap cannot corrupt the stream.
class SerialProxyTap {
public:
/// Bytes read from the device, before they are forwarded to any subscriber.
virtual void on_device_rx(const uint8_t *data, size_t len) = 0;
/// Bytes a subscriber sent towards the device, after they have been written.
virtual void on_client_tx(const uint8_t *data, size_t len) = 0;
/// True when the port must keep reading even with no subscriber attached, so a tap can
/// do its own protocol work while nobody is listening. Honoured only while no
/// subscriber holds the port; with one attached, the port mode alone decides.
virtual bool tap_needs_port() const = 0;
/// A client explicitly turned protocol handling off for this port. Distinct from the
/// automatic reset when a session ends: this one means a client intends to do something
/// else with the device -- reflash it, most likely -- so anything the tap believes about
/// it should be treated as suspect.
virtual void on_protocol_disabled() = 0;
};
#endif
class SerialProxy final : public uart::UARTDevice, public Component {
public:
void setup() override;
@@ -108,9 +77,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Get the port type
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
/// Handle a mode change requested by an API client
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode);
/// Configure UART parameters and apply them
/// @param api_connection The API connection requesting the change
/// @param baudrate Baud rate in bits per second
@@ -155,67 +121,13 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
#ifdef USE_SERIAL_PROXY_TAP
/// Attach a traffic observer. At most one, set once at setup time.
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
/// Write bytes originating from the tap rather than from a client. Bypasses the
/// subscriber ownership check, but only while the tap is being served bytes -- so a
/// port in RAW mode with a subscriber attached stays inert. Returns false when the
/// bytes were dropped for that reason.
bool write_from_tap(const uint8_t *data, size_t len) {
if (!this->tap_observing_()) {
return false;
}
this->write_array(data, len);
return true;
}
/// Whether the tap is currently being served bytes. Can flip false with no callback
/// (a subscriber attaching in RAW mode, say), so a tap should check before starting
/// protocol work and when a reply seems overdue.
bool tap_is_observed() const { return this->tap_observing_(); }
/// Resume reading after a tap's needs change. loop() disables itself when there is
/// neither a subscriber nor a tap that wants the port, so a tap starting fresh work
/// must ask for it back. Must be called from the main loop.
void tap_request_port() { this->enable_loop(); }
/// Whether the underlying device is present. On a USB UART this tracks enumeration, so
/// a tap can notice the device being unplugged and plugged back in.
bool is_device_connected() const { return this->parent_->is_connected(); }
/// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the
/// main loop is running -- during setup, for instance, while a component is still
/// blocking on can_proceed(). Must not be called from on_device_rx() or
/// on_client_tx(): each nested cycle costs a 256-byte stack frame.
void tap_pump();
#endif
protected:
#ifdef USE_API
/// Read from UART, hand the bytes to any tap, and forward them to a subscriber
/// (slow path with a 256-byte stack buffer)
/// Read from UART and send to API client (slow path with 256-byte stack buffer)
void read_and_send_(size_t available);
/// True when the given connection is the live subscriber. Every port operation
/// (write, configure, modem pins, flush, mode) requires this, so an unsubscribed
/// client can never share the wire with the subscriber or an active tap.
bool is_subscriber_(api::APIConnection *api_connection) const { return this->api_connection_ == api_connection; }
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Return the port to RAW when a subscriber goes away, so the mode never outlives it
void reset_mode_();
#else
/// Without a tap, PROTOCOL is refused, so the mode is fixed at RAW and there is
/// nothing to reset
void reset_mode_() {}
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// True when the tap should be shown the traffic passing through this port
bool tap_observing_() const;
/// True when a live subscriber other than the given connection holds the port
bool port_claimed_by_other_(api::APIConnection *api_connection) const;
#endif
/// Instance index for identifying this proxy in API messages
@@ -235,11 +147,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Port type
api::enums::SerialProxyPortType port_type_{};
#ifdef USE_SERIAL_PROXY_TAP
/// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW
api::enums::SerialProxyMode mode_{};
#endif
/// Optional GPIO pins for modem control
GPIOPin *rts_pin_{nullptr};
GPIOPin *dtr_pin_{nullptr};
@@ -247,10 +154,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Current modem pin states
bool rts_state_{false};
bool dtr_state_{false};
#ifdef USE_SERIAL_PROXY_TAP
SerialProxyTap *tap_{nullptr};
#endif
};
} // namespace esphome::serial_proxy
@@ -202,15 +202,8 @@ AudioPipelineState AudioPipeline::process_state() {
if (!this->is_playing_) {
// The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks
if (this->read_task_.is_created() || this->decode_task_.is_created()) {
// Both are attempted every time; a task that is still running on the other core is freed by a
// subsequent call, and freeing an already freed task succeeds without doing anything
bool read_task_freed = this->read_task_.deallocate();
bool decode_task_freed = this->decode_task_.deallocate();
if (!read_task_freed || !decode_task_freed) {
// A task is still running on the other core, so keep the pipeline in its current state and try
// again on the next call
return AudioPipelineState::PLAYING;
}
this->read_task_.deallocate();
this->decode_task_.deallocate();
if (this->hard_stop_) {
// Stop command was sent, so immediately end the playback
this->speaker_->stop();
@@ -322,17 +315,17 @@ void AudioPipeline::read_task(void *params) {
if (err == ESP_OK) {
size_t file_ring_buffer_size = this_pipeline->buffer_size_;
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock();
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer;
if (temp_ring_buffer == nullptr) {
if (!this_pipeline->raw_file_ring_buffer_.use_count()) {
temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size);
this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
}
if (temp_ring_buffer == nullptr) {
if (!this_pipeline->raw_file_ring_buffer_.use_count()) {
err = ESP_ERR_NO_MEM;
} else {
err = reader->add_sink(temp_ring_buffer);
reader->add_sink(this_pipeline->raw_file_ring_buffer_);
}
}
@@ -403,9 +396,7 @@ void AudioPipeline::decode_task(void *params) {
make_unique<audio::AudioDecoder>(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_);
esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_);
if (err == ESP_OK) {
err = decoder->add_source(this_pipeline->raw_file_ring_buffer_);
}
decoder->add_source(this_pipeline->raw_file_ring_buffer_);
if (err != ESP_OK) {
// Send specific error message
@@ -2,13 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_CONDITION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_STATE,
)
from esphome.const import CONF_CONDITION, CONF_ID, CONF_LAMBDA, CONF_STATE
from esphome.cpp_generator import LambdaExpression
from .. import template_ns
@@ -18,11 +12,7 @@ TemplateBinarySensor = template_ns.class_(
)
CONFIG_SCHEMA = (
cv.with_visibility(
binary_sensor.binary_sensor_schema(TemplateBinarySensor),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
binary_sensor.binary_sensor_schema(TemplateBinarySensor)
.extend(
{
cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda,
@@ -1,14 +1,10 @@
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS
from .. import template_ns
TemplateButton = template_ns.class_("TemplateButton", button.Button)
CONFIG_SCHEMA = cv.with_visibility(
button.button_schema(TemplateButton), cv.Visibility.UI, CONF_DEVICE_CLASS
)
CONFIG_SCHEMA = button.button_schema(TemplateButton)
async def to_code(config):
@@ -6,7 +6,6 @@ from esphome.const import (
CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION,
CONF_CURRENT_OPERATION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_OPEN_ACTION,
@@ -39,11 +38,7 @@ CONF_HAS_POSITION = "has_position"
CONF_TOGGLE_ACTION = "toggle_action"
CONFIG_SCHEMA = (
cv.with_visibility(
cover.cover_schema(TemplateCover),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
cover.cover_schema(TemplateCover)
.extend(
{
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import event
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS, CONF_EVENT_TYPES
from esphome.const import CONF_EVENT_TYPES
from .. import template_ns
@@ -9,9 +9,7 @@ CODEOWNERS = ["@nohat"]
TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component)
CONFIG_SCHEMA = cv.with_visibility(
event.event_schema(TemplateEvent), cv.Visibility.UI, CONF_DEVICE_CLASS
).extend(
CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend(
{
cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict),
}
@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
@@ -13,7 +12,6 @@ from esphome.const import (
CONF_RESTORE_VALUE,
CONF_SET_ACTION,
CONF_STEP,
CONF_UNIT_OF_MEASUREMENT,
)
from .. import template_ns
@@ -48,12 +46,7 @@ def validate(config):
CONFIG_SCHEMA = cv.All(
cv.with_visibility(
number.number_schema(TemplateNumber),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
number.number_schema(TemplateNumber)
.extend(
{
cv.Required(CONF_MAX_VALUE): cv.float_,
+4 -18
View File
@@ -2,16 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
CONF_ID,
CONF_LAMBDA,
CONF_STATE,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
from .. import template_ns
@@ -20,14 +11,9 @@ TemplateSensor = template_ns.class_(
)
CONFIG_SCHEMA = (
cv.with_visibility(
sensor.sensor_schema(TemplateSensor, accuracy_decimals=1),
cv.Visibility.UI,
CONF_UNIT_OF_MEASUREMENT,
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_STATE_CLASS,
CONF_FORCE_UPDATE,
sensor.sensor_schema(
TemplateSensor,
accuracy_decimals=1,
)
.extend(
{
@@ -4,7 +4,6 @@ from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import (
CONF_ASSUMED_STATE,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_OPTIMISTIC,
@@ -32,11 +31,7 @@ def validate(config):
CONFIG_SCHEMA = cv.All(
cv.with_visibility(
switch.switch_schema(TemplateSwitch),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
switch.switch_schema(TemplateSwitch)
.extend(
{
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
@@ -3,7 +3,7 @@ import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components.text_sensor import TextSensorPublishAction
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_STATE
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
from .. import template_ns
@@ -12,11 +12,7 @@ TemplateTextSensor = template_ns.class_(
)
CONFIG_SCHEMA = (
cv.with_visibility(
text_sensor.text_sensor_schema(),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
text_sensor.text_sensor_schema()
.extend(
{
cv.GenerateID(): cv.declare_id(TemplateTextSensor),
@@ -6,7 +6,6 @@ from esphome.const import (
CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION,
CONF_CURRENT_OPERATION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_OPEN_ACTION,
@@ -37,11 +36,7 @@ CONF_HAS_POSITION = "has_position"
CONF_TOGGLE_ACTION = "toggle_action"
CONFIG_SCHEMA = (
cv.with_visibility(
valve.valve_schema(TemplateValve),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
valve.valve_schema(TemplateValve)
.extend(
{
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
+1 -2
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir, remote_base
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_MODEL
from esphome.types import ConfigType
@@ -26,6 +26,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).exten
async def to_code(config: ConfigType) -> None:
remote_base.request_protocol("toshiba_ac") # used from C++
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_model(config[CONF_MODEL]))
@@ -1,7 +1,6 @@
#include "zigbee_time_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME)
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::zigbee {
@@ -48,7 +47,6 @@ void ZigbeeTime::set_epoch_time(uint32_t epoch) {
this->synchronize_epoch_(epoch);
this->has_time_ = true;
});
App.wake_loop_threadsafe();
}
void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) {
+1 -4
View File
@@ -49,8 +49,7 @@ void ZigbeeComponent::factory_reset() {
void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) {
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
global_zigbee->set_timeout("zb_init", 100, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
App.wake_loop_threadsafe();
global_zigbee->set_timeout("zb_init", 10, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
return;
}
if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) {
@@ -89,7 +88,6 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
global_zigbee->set_timeout("zb_init", 1000, []() {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION);
});
App.wake_loop_threadsafe();
}
} break;
case EZB_BDB_SIGNAL_STEERING: {
@@ -115,7 +113,6 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING);
});
}
App.wake_loop_threadsafe();
}
} break;
case EZB_ZDO_SIGNAL_LEAVE: {
+2 -4
View File
@@ -1,10 +1,10 @@
#include "zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"
extern "C" {
#include <zboss_api.h>
@@ -120,7 +120,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
/* Set default response value. */
p_device_cb_param->status = RET_OK;
App.wake_loop_threadsafe();
esphome::wake_loop_threadsafe();
// endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) {
@@ -138,7 +138,6 @@ void ZigbeeComponent::on_join_(bool factory_new) {
ESP_LOGD(TAG, "Joined the network");
this->join_cb_.call(factory_new);
});
App.wake_loop_threadsafe();
}
void ZigbeeComponent::on_start_() {
@@ -146,7 +145,6 @@ void ZigbeeComponent::on_start_() {
ESP_LOGD(TAG, "Started zigbee stack");
this->start_cb_.call();
});
App.wake_loop_threadsafe();
}
#ifdef USE_ZIGBEE_WIPE_ON_BOOT

Some files were not shown because too many files have changed in this diff Show More