From 524cd4b4e357477f55a124709f00a47cf3a8bd87 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 5 May 2025 07:29:17 +1000 Subject: [PATCH] [packet_transport] Extract packet encoding functionality (#8187) --- CODEOWNERS | 1 + .../components/packet_transport/__init__.py | 201 +++++++ .../packet_transport/binary_sensor.py | 19 + .../packet_transport/packet_transport.cpp | 534 ++++++++++++++++++ .../packet_transport/packet_transport.h | 155 +++++ esphome/components/packet_transport/sensor.py | 19 + esphome/components/udp/__init__.py | 262 +++++---- esphome/components/udp/automation.h | 38 ++ esphome/components/udp/binary_sensor.py | 28 +- .../udp/packet_transport/__init__.py | 29 + .../udp/packet_transport/udp_transport.cpp | 36 ++ .../udp/packet_transport/udp_transport.h | 26 + esphome/components/udp/sensor.py | 28 +- esphome/components/udp/udp_component.cpp | 496 +--------------- esphome/components/udp/udp_component.h | 149 +---- tests/components/packet_transport/common.yaml | 40 ++ .../packet_transport/test.bk72xx-ard.yaml | 1 + .../packet_transport/test.esp32-ard.yaml | 1 + .../packet_transport/test.esp32-c3-ard.yaml | 1 + .../packet_transport/test.esp32-c3-idf.yaml | 1 + .../packet_transport/test.esp32-idf.yaml | 1 + .../packet_transport/test.esp8266-ard.yaml | 1 + .../packet_transport/test.host.yaml | 4 + .../packet_transport/test.rp2040-ard.yaml | 1 + tests/components/udp/common.yaml | 42 +- 25 files changed, 1305 insertions(+), 809 deletions(-) create mode 100644 esphome/components/packet_transport/__init__.py create mode 100644 esphome/components/packet_transport/binary_sensor.py create mode 100644 esphome/components/packet_transport/packet_transport.cpp create mode 100644 esphome/components/packet_transport/packet_transport.h create mode 100644 esphome/components/packet_transport/sensor.py create mode 100644 esphome/components/udp/automation.h create mode 100644 esphome/components/udp/packet_transport/__init__.py create mode 100644 esphome/components/udp/packet_transport/udp_transport.cpp create mode 100644 esphome/components/udp/packet_transport/udp_transport.h create mode 100644 tests/components/packet_transport/common.yaml create mode 100644 tests/components/packet_transport/test.bk72xx-ard.yaml create mode 100644 tests/components/packet_transport/test.esp32-ard.yaml create mode 100644 tests/components/packet_transport/test.esp32-c3-ard.yaml create mode 100644 tests/components/packet_transport/test.esp32-c3-idf.yaml create mode 100644 tests/components/packet_transport/test.esp32-idf.yaml create mode 100644 tests/components/packet_transport/test.esp8266-ard.yaml create mode 100644 tests/components/packet_transport/test.host.yaml create mode 100644 tests/components/packet_transport/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 06d3601858..46e0e6c579 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -319,6 +319,7 @@ esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core +esphome/components/packet_transport/* @clydebarrow esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py new file mode 100644 index 0000000000..99c1d824ca --- /dev/null +++ b/esphome/components/packet_transport/__init__.py @@ -0,0 +1,201 @@ +"""ESPHome packet transport component.""" + +import hashlib +import logging + +import esphome.codegen as cg +from esphome.components.api import CONF_ENCRYPTION +from esphome.components.binary_sensor import BinarySensor +from esphome.components.sensor import Sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BINARY_SENSORS, + CONF_ID, + CONF_INTERNAL, + CONF_KEY, + CONF_NAME, + CONF_PLATFORM, + CONF_SENSORS, +) +from esphome.core import CORE +from esphome.cpp_generator import MockObjClass + +CODEOWNERS = ["@clydebarrow"] +AUTO_LOAD = ["xxtea"] + +packet_transport_ns = cg.esphome_ns.namespace("packet_transport") +PacketTransport = packet_transport_ns.class_("PacketTransport", cg.PollingComponent) + +IS_PLATFORM_COMPONENT = True + +DOMAIN = "packet_transport" +CONF_BROADCAST = "broadcast" +CONF_BROADCAST_ID = "broadcast_id" +CONF_PROVIDER = "provider" +CONF_PROVIDERS = "providers" +CONF_REMOTE_ID = "remote_id" +CONF_PING_PONG_ENABLE = "ping_pong_enable" +CONF_PING_PONG_RECYCLE_TIME = "ping_pong_recycle_time" +CONF_ROLLING_CODE_ENABLE = "rolling_code_enable" +CONF_TRANSPORT_ID = "transport_id" + + +_LOGGER = logging.getLogger(__name__) + + +def sensor_validation(cls: MockObjClass): + return cv.maybe_simple_value( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(cls), + cv.Optional(CONF_BROADCAST_ID): cv.validate_id_name, + } + ), + key=CONF_ID, + ) + + +def provider_name_validate(value): + value = cv.valid_name(value) + if "_" in value: + _LOGGER.warning( + "Device names typically do not contain underscores - did you mean to use a hyphen in '%s'?", + value, + ) + return value + + +ENCRYPTION_SCHEMA = { + cv.Optional(CONF_ENCRYPTION): cv.maybe_simple_value( + cv.Schema( + { + cv.Required(CONF_KEY): cv.string, + } + ), + key=CONF_KEY, + ) +} + +PROVIDER_SCHEMA = cv.Schema( + { + cv.Required(CONF_NAME): provider_name_validate, + } +).extend(ENCRYPTION_SCHEMA) + + +def validate_(config): + if CONF_ENCRYPTION in config: + if CONF_SENSORS not in config and CONF_BINARY_SENSORS not in config: + raise cv.Invalid("No sensors or binary sensors to encrypt") + elif config[CONF_ROLLING_CODE_ENABLE]: + raise cv.Invalid("Rolling code requires an encryption key") + if config[CONF_PING_PONG_ENABLE]: + if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()): + raise cv.Invalid("Ping-pong requires at least one encrypted provider") + return config + + +TRANSPORT_SCHEMA = ( + cv.polling_component_schema("15s") + .extend( + { + cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, + cv.Optional( + CONF_PING_PONG_RECYCLE_TIME, default="600s" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_SENSORS): cv.ensure_list(sensor_validation(Sensor)), + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + sensor_validation(BinarySensor) + ), + cv.Optional(CONF_PROVIDERS, default=[]): cv.ensure_list(PROVIDER_SCHEMA), + }, + ) + .extend(ENCRYPTION_SCHEMA) + .add_extra(validate_) +) + + +def transport_schema(cls): + return TRANSPORT_SCHEMA.extend({cv.GenerateID(): cv.declare_id(cls)}) + + +# Build a list of sensors for this platform +CORE.data[DOMAIN] = {CONF_SENSORS: []} + + +def get_sensors(transport_id): + """Return the list of sensors for this platform.""" + return ( + sensor + for sensor in CORE.data[DOMAIN][CONF_SENSORS] + if sensor[CONF_TRANSPORT_ID] == transport_id + ) + + +def validate_packet_transport_sensor(config): + if CONF_NAME in config and CONF_INTERNAL not in config: + raise cv.Invalid("Must provide internal: config when using name:") + CORE.data[DOMAIN][CONF_SENSORS].append(config) + return config + + +def packet_transport_sensor_schema(base_schema): + return cv.All( + base_schema.extend( + { + cv.GenerateID(CONF_TRANSPORT_ID): cv.use_id(PacketTransport), + cv.Optional(CONF_REMOTE_ID): cv.string_strict, + cv.Required(CONF_PROVIDER): provider_name_validate, + } + ), + cv.has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), + validate_packet_transport_sensor, + ) + + +def hash_encryption_key(config: dict): + return list(hashlib.sha256(config[CONF_KEY].encode()).digest()) + + +async def register_packet_transport(var, config): + var = await cg.register_component(var, config) + cg.add(var.set_rolling_code_enable(config[CONF_ROLLING_CODE_ENABLE])) + cg.add(var.set_ping_pong_enable(config[CONF_PING_PONG_ENABLE])) + cg.add( + var.set_ping_pong_recycle_time( + config[CONF_PING_PONG_RECYCLE_TIME].total_seconds + ) + ) + # Get directly configured providers, plus those from sensors and binary sensors + providers = { + sensor[CONF_PROVIDER] for sensor in get_sensors(config[CONF_ID]) + }.union(x[CONF_NAME] for x in config[CONF_PROVIDERS]) + for provider in providers: + cg.add(var.add_provider(provider)) + for provider in config[CONF_PROVIDERS]: + name = provider[CONF_NAME] + if encryption := provider.get(CONF_ENCRYPTION): + cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) + + for sens_conf in config.get(CONF_SENSORS, ()): + sens_id = sens_conf[CONF_ID] + sensor = await cg.get_variable(sens_id) + bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) + cg.add(var.add_sensor(bcst_id, sensor)) + for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + sens_id = sens_conf[CONF_ID] + sensor = await cg.get_variable(sens_id) + bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) + cg.add(var.add_binary_sensor(bcst_id, sensor)) + + if encryption := config.get(CONF_ENCRYPTION): + cg.add(var.set_encryption_key(hash_encryption_key(encryption))) + return providers + + +async def new_packet_transport(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_platform_name(config[CONF_PLATFORM])) + providers = await register_packet_transport(var, config) + return var, providers diff --git a/esphome/components/packet_transport/binary_sensor.py b/esphome/components/packet_transport/binary_sensor.py new file mode 100644 index 0000000000..076e37e6bb --- /dev/null +++ b/esphome/components/packet_transport/binary_sensor.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +from esphome.const import CONF_ID + +from . import ( + CONF_PROVIDER, + CONF_REMOTE_ID, + CONF_TRANSPORT_ID, + packet_transport_sensor_schema, +) + +CONFIG_SCHEMA = packet_transport_sensor_schema(binary_sensor.binary_sensor_schema()) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + comp = await cg.get_variable(config[CONF_TRANSPORT_ID]) + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp new file mode 100644 index 0000000000..4514584408 --- /dev/null +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -0,0 +1,534 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "packet_transport.h" + +#include "esphome/components/xxtea/xxtea.h" + +namespace esphome { +namespace packet_transport { +/** + * Structure of a data packet; everything is little-endian + * + * --- In clear text --- + * MAGIC_NUMBER: 16 bits + * host name length: 1 byte + * host name: (length) bytes + * padding: 0 or more null bytes to a 4 byte boundary + * + * --- Encrypted (if key set) ---- + * DATA_KEY: 1 byte: OR ROLLING_CODE_KEY: + * Rolling code (if enabled): 8 bytes + * Ping keys: if any + * repeat: + * PING_KEY: 1 byte + * ping code: 4 bytes + * Sensors: + * repeat: + * SENSOR_KEY: 1 byte + * float value: 4 bytes + * name length: 1 byte + * name + * Binary Sensors: + * repeat: + * BINARY_SENSOR_KEY: 1 byte + * bool value: 1 bytes + * name length: 1 byte + * name + * + * Padded to a 4 byte boundary with nulls + * + * Structure of a ping request packet: + * --- In clear text --- + * MAGIC_PING: 16 bits + * host name length: 1 byte + * host name: (length) bytes + * Ping key (4 bytes) + * + */ +static const char *const TAG = "packet_transport"; + +static size_t round4(size_t value) { return (value + 3) & ~3; } + +union FuData { + uint32_t u32; + float f32; +}; + +static const uint16_t MAGIC_NUMBER = 0x4553; +static const uint16_t MAGIC_PING = 0x5048; +static const uint32_t PREF_HASH = 0x45535043; +enum DataKey { + ZERO_FILL_KEY, + DATA_KEY, + SENSOR_KEY, + BINARY_SENSOR_KEY, + PING_KEY, + ROLLING_CODE_KEY, +}; + +enum DecodeResult { + DECODE_OK, + DECODE_UNMATCHED, + DECODE_ERROR, + DECODE_EMPTY, +}; + +static const size_t MAX_PING_KEYS = 4; + +static inline void add(std::vector &vec, uint32_t data) { + vec.push_back(data & 0xFF); + vec.push_back((data >> 8) & 0xFF); + vec.push_back((data >> 16) & 0xFF); + vec.push_back((data >> 24) & 0xFF); +} + +class PacketDecoder { + public: + PacketDecoder(const uint8_t *buffer, size_t len) : buffer_(buffer), len_(len) {} + + DecodeResult decode_string(char *data, size_t maxlen) { + if (this->position_ == this->len_) + return DECODE_EMPTY; + auto len = this->buffer_[this->position_]; + if (len == 0 || this->position_ + 1 + len > this->len_ || len >= maxlen) + return DECODE_ERROR; + this->position_++; + memcpy(data, this->buffer_ + this->position_, len); + data[len] = 0; + this->position_ += len; + return DECODE_OK; + } + + template DecodeResult get(T &data) { + if (this->position_ + sizeof(T) > this->len_) + return DECODE_ERROR; + T value = 0; + for (size_t i = 0; i != sizeof(T); ++i) { + value += this->buffer_[this->position_++] << (i * 8); + } + data = value; + return DECODE_OK; + } + + template DecodeResult decode(uint8_t key, T &data) { + if (this->position_ == this->len_) + return DECODE_EMPTY; + if (this->buffer_[this->position_] != key) + return DECODE_UNMATCHED; + if (this->position_ + 1 + sizeof(T) > this->len_) + return DECODE_ERROR; + this->position_++; + T value = 0; + for (size_t i = 0; i != sizeof(T); ++i) { + value += this->buffer_[this->position_++] << (i * 8); + } + data = value; + return DECODE_OK; + } + + template DecodeResult decode(uint8_t key, char *buf, size_t buflen, T &data) { + if (this->position_ == this->len_) + return DECODE_EMPTY; + if (this->buffer_[this->position_] != key) + return DECODE_UNMATCHED; + this->position_++; + T value = 0; + for (size_t i = 0; i != sizeof(T); ++i) { + value += this->buffer_[this->position_++] << (i * 8); + } + data = value; + return this->decode_string(buf, buflen); + } + + DecodeResult decode(uint8_t key) { + if (this->position_ == this->len_) + return DECODE_EMPTY; + if (this->buffer_[this->position_] != key) + return DECODE_UNMATCHED; + this->position_++; + return DECODE_OK; + } + + size_t get_remaining_size() const { return this->len_ - this->position_; } + + // align the pointer to the given byte boundary + bool bump_to(size_t boundary) { + auto newpos = this->position_; + auto offset = this->position_ % boundary; + if (offset != 0) { + newpos += boundary - offset; + } + if (newpos >= this->len_) + return false; + this->position_ = newpos; + return true; + } + + bool decrypt(const uint32_t *key) { + if (this->get_remaining_size() % 4 != 0) { + return false; + } + xxtea::decrypt((uint32_t *) (this->buffer_ + this->position_), this->get_remaining_size() / 4, key); + return true; + } + + protected: + const uint8_t *buffer_; + size_t len_; + size_t position_{}; +}; + +static inline void add(std::vector &vec, uint8_t data) { vec.push_back(data); } +static inline void add(std::vector &vec, uint16_t data) { + vec.push_back((uint8_t) data); + vec.push_back((uint8_t) (data >> 8)); +} +static inline void add(std::vector &vec, DataKey data) { vec.push_back(data); } +static void add(std::vector &vec, const char *str) { + auto len = strlen(str); + vec.push_back(len); + for (size_t i = 0; i != len; i++) { + vec.push_back(*str++); + } +} + +void PacketTransport::setup() { + this->name_ = App.get_name().c_str(); + if (strlen(this->name_) > 255) { + this->mark_failed(); + this->status_set_error("Device name exceeds 255 chars"); + return; + } + this->resend_ping_key_ = this->ping_pong_enable_; + this->pref_ = global_preferences->make_preference(PREF_HASH, true); + if (this->rolling_code_enable_) { + // restore the upper 32 bits of the rolling code, increment and save. + this->pref_.load(&this->rolling_code_[1]); + this->rolling_code_[1]++; + this->pref_.save(&this->rolling_code_[1]); + // must make sure it's saved immediately + global_preferences->sync(); + this->ping_key_ = random_uint32(); + ESP_LOGV(TAG, "Rolling code incremented, upper part now %u", (unsigned) this->rolling_code_[1]); + } +#ifdef USE_SENSOR + for (auto &sensor : this->sensors_) { + sensor.sensor->add_on_state_callback([this, &sensor](float x) { + this->updated_ = true; + sensor.updated = true; + }); + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->binary_sensors_) { + sensor.sensor->add_on_state_callback([this, &sensor](bool value) { + this->updated_ = true; + sensor.updated = true; + }); + } +#endif + // initialise the header. This is invariant. + add(this->header_, MAGIC_NUMBER); + add(this->header_, this->name_); + // pad to a multiple of 4 bytes + while (this->header_.size() & 0x3) + this->header_.push_back(0); +} + +void PacketTransport::init_data_() { + this->data_.clear(); + if (this->rolling_code_enable_) { + add(this->data_, ROLLING_CODE_KEY); + add(this->data_, this->rolling_code_[0]); + add(this->data_, this->rolling_code_[1]); + this->increment_code_(); + } else { + add(this->data_, DATA_KEY); + } + for (auto pkey : this->ping_keys_) { + add(this->data_, PING_KEY); + add(this->data_, pkey.second); + } +} + +void PacketTransport::flush_() { + if (!this->should_send() || this->data_.empty()) + return; + auto header_len = round4(this->header_.size()); + auto len = round4(data_.size()); + auto encode_buffer = std::vector(round4(header_len + len)); + memcpy(encode_buffer.data(), this->header_.data(), this->header_.size()); + memcpy(encode_buffer.data() + header_len, this->data_.data(), this->data_.size()); + if (this->is_encrypted_()) { + xxtea::encrypt((uint32_t *) (encode_buffer.data() + header_len), len / 4, + (uint32_t *) this->encryption_key_.data()); + } + this->send_packet(encode_buffer); +} + +void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) { + auto len = 1 + 1 + 1 + strlen(id); + if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { + this->flush_(); + } + add(this->data_, key); + add(this->data_, (uint8_t) data); + add(this->data_, id); +} +void PacketTransport::add_data_(uint8_t key, const char *id, float data) { + FuData udata{.f32 = data}; + this->add_data_(key, id, udata.u32); +} + +void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) { + auto len = 4 + 1 + 1 + strlen(id); + if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { + this->flush_(); + } + add(this->data_, key); + add(this->data_, data); + add(this->data_, id); +} +void PacketTransport::send_data_(bool all) { + if (!this->should_send()) + return; + this->init_data_(); +#ifdef USE_SENSOR + for (auto &sensor : this->sensors_) { + if (all || sensor.updated) { + sensor.updated = false; + this->add_data_(SENSOR_KEY, sensor.id, sensor.sensor->get_state()); + } + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->binary_sensors_) { + if (all || sensor.updated) { + sensor.updated = false; + this->add_binary_data_(BINARY_SENSOR_KEY, sensor.id, sensor.sensor->state); + } + } +#endif + this->flush_(); + this->updated_ = false; +} + +void PacketTransport::update() { + auto now = millis() / 1000; + if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { + this->resend_ping_key_ = this->ping_pong_enable_; + this->last_key_time_ = now; + } +} + +void PacketTransport::add_key_(const char *name, uint32_t key) { + if (!this->is_encrypted_()) + return; + if (this->ping_keys_.count(name) == 0 && this->ping_keys_.size() == MAX_PING_KEYS) { + ESP_LOGW(TAG, "Ping key from %s discarded", name); + return; + } + this->ping_keys_[name] = key; + this->updated_ = true; + ESP_LOGV(TAG, "Ping key from %s now %X", name, (unsigned) key); +} + +static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) { + uint32_t code0, code1; + if (decoder.get(code0) != DECODE_OK || decoder.get(code1) != DECODE_OK) { + ESP_LOGW(TAG, "Rolling code requires 8 bytes"); + return false; + } + if (code1 < provider.last_code[1] || (code1 == provider.last_code[1] && code0 <= provider.last_code[0])) { + ESP_LOGW(TAG, "Rolling code for %s %08lX:%08lX is old", provider.name, (unsigned long) code1, + (unsigned long) code0); + return false; + } + provider.last_code[0] = code0; + provider.last_code[1] = code1; + ESP_LOGV(TAG, "Saw new rolling code for %s %08lX:%08lX", provider.name, (unsigned long) code1, (unsigned long) code0); + return true; +} + +/** + * Process a received packet + */ +void PacketTransport::process_(std::vector &data) { + auto ping_key_seen = !this->ping_pong_enable_; + PacketDecoder decoder((data.data()), data.size()); + char namebuf[256]{}; + uint8_t byte; + FuData rdata{}; + uint16_t magic; + if (decoder.get(magic) != DECODE_OK) { + ESP_LOGD(TAG, "Short buffer"); + return; + } + if (magic != MAGIC_NUMBER && magic != MAGIC_PING) { + ESP_LOGV(TAG, "Bad magic %X", magic); + return; + } + + if (decoder.decode_string(namebuf, sizeof namebuf) != DECODE_OK) { + ESP_LOGV(TAG, "Bad hostname length"); + return; + } + if (strcmp(this->name_, namebuf) == 0) { + ESP_LOGVV(TAG, "Ignoring our own data"); + return; + } + if (magic == MAGIC_PING) { + uint32_t key; + if (decoder.get(key) != DECODE_OK) { + ESP_LOGW(TAG, "Bad ping request"); + return; + } + this->add_key_(namebuf, key); + ESP_LOGV(TAG, "Updated ping key for %s to %08X", namebuf, (unsigned) key); + return; + } + + if (this->providers_.count(namebuf) == 0) { + ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); + return; + } + ESP_LOGV(TAG, "Found hostname %s", namebuf); + +#ifdef USE_SENSOR + auto &sensors = this->remote_sensors_[namebuf]; +#endif +#ifdef USE_BINARY_SENSOR + auto &binary_sensors = this->remote_binary_sensors_[namebuf]; +#endif + + if (!decoder.bump_to(4)) { + ESP_LOGW(TAG, "Bad packet length %zu", data.size()); + } + auto len = decoder.get_remaining_size(); + if (round4(len) != len) { + ESP_LOGW(TAG, "Bad payload length %zu", len); + return; + } + + auto &provider = this->providers_[namebuf]; + // if encryption not used with this host, ping check is pointless since it would be easily spoofed. + if (provider.encryption_key.empty()) + ping_key_seen = true; + + if (!provider.encryption_key.empty()) { + decoder.decrypt((const uint32_t *) provider.encryption_key.data()); + } + if (decoder.get(byte) != DECODE_OK) { + ESP_LOGV(TAG, "No key byte"); + return; + } + + if (byte == ROLLING_CODE_KEY) { + if (!process_rolling_code(provider, decoder)) + return; + } else if (byte != DATA_KEY) { + ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte); + return; + } + uint32_t key; + while (decoder.get_remaining_size() != 0) { + if (decoder.decode(ZERO_FILL_KEY) == DECODE_OK) + continue; + if (decoder.decode(PING_KEY, key) == DECODE_OK) { + if (key == this->ping_key_) { + ping_key_seen = true; + ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key); + } else { + ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); + } + continue; + } + if (!ping_key_seen) { + ESP_LOGW(TAG, "Ping key not seen"); + this->resend_ping_key_ = true; + break; + } + if (decoder.decode(BINARY_SENSOR_KEY, namebuf, sizeof(namebuf), byte) == DECODE_OK) { + ESP_LOGV(TAG, "Got binary sensor %s %d", namebuf, byte); +#ifdef USE_BINARY_SENSOR + if (binary_sensors.count(namebuf) != 0) + binary_sensors[namebuf]->publish_state(byte != 0); +#endif + continue; + } + if (decoder.decode(SENSOR_KEY, namebuf, sizeof(namebuf), rdata.u32) == DECODE_OK) { + ESP_LOGV(TAG, "Got sensor %s %f", namebuf, rdata.f32); +#ifdef USE_SENSOR + if (sensors.count(namebuf) != 0) + sensors[namebuf]->publish_state(rdata.f32); +#endif + continue; + } + if (decoder.get(byte) == DECODE_OK) { + ESP_LOGW(TAG, "Unknown key %X", byte); + ESP_LOGD(TAG, "Buffer pos: %zu contents: %s", data.size() - decoder.get_remaining_size(), + format_hex_pretty(data).c_str()); + } + break; + } +} + +void PacketTransport::dump_config() { + ESP_LOGCONFIG(TAG, "Packet Transport:"); + ESP_LOGCONFIG(TAG, " Platform: %s", this->platform_name_); + ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(this->is_encrypted_())); + ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); +#ifdef USE_SENSOR + for (auto sensor : this->sensors_) + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); +#endif +#ifdef USE_BINARY_SENSOR + for (auto sensor : this->binary_sensors_) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); +#endif + for (const auto &host : this->providers_) { + ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); + ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(!host.second.encryption_key.empty())); +#ifdef USE_SENSOR + for (const auto &sensor : this->remote_sensors_[host.first.c_str()]) + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.first.c_str()); +#endif +#ifdef USE_BINARY_SENSOR + for (const auto &sensor : this->remote_binary_sensors_[host.first.c_str()]) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.first.c_str()); +#endif + } +} +void PacketTransport::increment_code_() { + if (this->rolling_code_enable_) { + if (++this->rolling_code_[0] == 0) { + this->rolling_code_[1]++; + this->pref_.save(&this->rolling_code_[1]); + // must make sure it's saved immediately + global_preferences->sync(); + } + } +} + +void PacketTransport::loop() { + if (this->resend_ping_key_) + this->send_ping_pong_request_(); + if (this->updated_) { + this->send_data_(this->resend_data_); + } +} + +void PacketTransport::send_ping_pong_request_() { + if (!this->ping_pong_enable_ || !this->should_send()) + return; + this->ping_key_ = random_uint32(); + this->ping_header_.clear(); + add(this->ping_header_, MAGIC_PING); + add(this->ping_header_, this->name_); + add(this->ping_header_, this->ping_key_); + this->send_packet(this->ping_header_); + this->resend_ping_key_ = false; + ESP_LOGV(TAG, "Sent new ping request %08X", (unsigned) this->ping_key_); +} +} // namespace packet_transport +} // namespace esphome diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h new file mode 100644 index 0000000000..6799cb6ea1 --- /dev/null +++ b/esphome/components/packet_transport/packet_transport.h @@ -0,0 +1,155 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +# +#include +#include + +/** + * Providing packet encoding functions for exchanging data with a remote host. + * + * A transport is required to send the data; this is provided by a child class. + * The child class should implement the virtual functions send_packet_ and get_max_packet_size_. + * On receipt of a data packet, it should call `this->process_()` with the data. + */ + +namespace esphome { +namespace packet_transport { + +struct Provider { + std::vector encryption_key; + const char *name; + uint32_t last_code[2]; +}; + +#ifdef USE_SENSOR +struct Sensor { + sensor::Sensor *sensor; + const char *id; + bool updated; +}; +#endif +#ifdef USE_BINARY_SENSOR +struct BinarySensor { + binary_sensor::BinarySensor *sensor; + const char *id; + bool updated; +}; +#endif + +class PacketTransport : public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void dump_config() override; + +#ifdef USE_SENSOR + void add_sensor(const char *id, sensor::Sensor *sensor) { + Sensor st{sensor, id, true}; + this->sensors_.push_back(st); + } + void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { + this->add_provider(hostname); + this->remote_sensors_[hostname][remote_id] = sensor; + } +#endif +#ifdef USE_BINARY_SENSOR + void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { + BinarySensor st{sensor, id, true}; + this->binary_sensors_.push_back(st); + } + + void add_remote_binary_sensor(const char *hostname, const char *remote_id, binary_sensor::BinarySensor *sensor) { + this->add_provider(hostname); + this->remote_binary_sensors_[hostname][remote_id] = sensor; + } +#endif + + void add_provider(const char *hostname) { + if (this->providers_.count(hostname) == 0) { + Provider provider; + provider.encryption_key = std::vector{}; + provider.last_code[0] = 0; + provider.last_code[1] = 0; + provider.name = hostname; + this->providers_[hostname] = provider; +#ifdef USE_SENSOR + this->remote_sensors_[hostname] = std::map(); +#endif +#ifdef USE_BINARY_SENSOR + this->remote_binary_sensors_[hostname] = std::map(); +#endif + } + } + + void set_encryption_key(std::vector key) { this->encryption_key_ = std::move(key); } + void set_rolling_code_enable(bool enable) { this->rolling_code_enable_ = enable; } + void set_ping_pong_enable(bool enable) { this->ping_pong_enable_ = enable; } + void set_ping_pong_recycle_time(uint32_t recycle_time) { this->ping_pong_recyle_time_ = recycle_time; } + void set_provider_encryption(const char *name, std::vector key) { + this->providers_[name].encryption_key = std::move(key); + } + void set_platform_name(const char *name) { this->platform_name_ = name; } + + protected: + // child classes must implement this + virtual void send_packet(std::vector &buf) const = 0; + virtual size_t get_max_packet_size() = 0; + virtual bool should_send() { return true; } + + // to be called by child classes when a data packet is received. + void process_(std::vector &data); + void send_data_(bool all); + void flush_(); + void add_data_(uint8_t key, const char *id, float data); + void add_data_(uint8_t key, const char *id, uint32_t data); + void increment_code_(); + void add_binary_data_(uint8_t key, const char *id, bool data); + void init_data_(); + + bool updated_{}; + uint32_t ping_key_{}; + uint32_t rolling_code_[2]{}; + bool rolling_code_enable_{}; + bool ping_pong_enable_{}; + uint32_t ping_pong_recyle_time_{}; + uint32_t last_key_time_{}; + bool resend_ping_key_{}; + bool resend_data_{}; + const char *name_{}; + ESPPreferenceObject pref_{}; + + std::vector encryption_key_{}; + +#ifdef USE_SENSOR + std::vector sensors_{}; + std::map> remote_sensors_{}; +#endif +#ifdef USE_BINARY_SENSOR + std::vector binary_sensors_{}; + std::map> remote_binary_sensors_{}; +#endif + + std::map providers_{}; + std::vector ping_header_{}; + std::vector header_{}; + std::vector data_{}; + std::map ping_keys_{}; + const char *platform_name_{""}; + void add_key_(const char *name, uint32_t key); + void send_ping_pong_request_(); + void process_ping_request_(const char *name, uint8_t *ptr, size_t len); + + inline bool is_encrypted_() { return !this->encryption_key_.empty(); } +}; + +} // namespace packet_transport +} // namespace esphome diff --git a/esphome/components/packet_transport/sensor.py b/esphome/components/packet_transport/sensor.py new file mode 100644 index 0000000000..15c0e33b30 --- /dev/null +++ b/esphome/components/packet_transport/sensor.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components.sensor import new_sensor, sensor_schema +from esphome.const import CONF_ID + +from . import ( + CONF_PROVIDER, + CONF_REMOTE_ID, + CONF_TRANSPORT_ID, + packet_transport_sensor_schema, +) + +CONFIG_SCHEMA = packet_transport_sensor_schema(sensor_schema()) + + +async def to_code(config): + var = await new_sensor(config) + comp = await cg.get_variable(config[CONF_TRANSPORT_ID]) + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 140d1e4236..ed405d7c22 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,164 +1,162 @@ -import hashlib - +from esphome import automation +from esphome.automation import Trigger import esphome.codegen as cg -from esphome.components.api import CONF_ENCRYPTION -from esphome.components.binary_sensor import BinarySensor -from esphome.components.sensor import Sensor -import esphome.config_validation as cv -from esphome.const import ( +from esphome.components.packet_transport import ( CONF_BINARY_SENSORS, - CONF_ID, - CONF_INTERNAL, - CONF_KEY, - CONF_NAME, - CONF_PORT, + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, + CONF_PROVIDERS, + CONF_ROLLING_CODE_ENABLE, CONF_SENSORS, ) -from esphome.cpp_generator import MockObjClass +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID +from esphome.core import Lambda +from esphome.cpp_generator import ExpressionStatement, MockObj CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket", "xxtea"] +AUTO_LOAD = ["socket"] + MULTI_CONF = True - udp_ns = cg.esphome_ns.namespace("udp") -UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent) +UDPComponent = udp_ns.class_("UDPComponent", cg.Component) +UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) +trigger_args = cg.std_vector.template(cg.uint8) -CONF_BROADCAST = "broadcast" -CONF_BROADCAST_ID = "broadcast_id" CONF_ADDRESSES = "addresses" CONF_LISTEN_ADDRESS = "listen_address" -CONF_PROVIDER = "provider" -CONF_PROVIDERS = "providers" -CONF_REMOTE_ID = "remote_id" CONF_UDP_ID = "udp_id" -CONF_PING_PONG_ENABLE = "ping_pong_enable" -CONF_PING_PONG_RECYCLE_TIME = "ping_pong_recycle_time" -CONF_ROLLING_CODE_ENABLE = "rolling_code_enable" +CONF_ON_RECEIVE = "on_receive" +CONF_LISTEN_PORT = "listen_port" +CONF_BROADCAST_PORT = "broadcast_port" - -def sensor_validation(cls: MockObjClass): - return cv.maybe_simple_value( - cv.Schema( - { - cv.Required(CONF_ID): cv.use_id(cls), - cv.Optional(CONF_BROADCAST_ID): cv.validate_id_name, - } - ), - key=CONF_ID, - ) - - -ENCRYPTION_SCHEMA = { - cv.Optional(CONF_ENCRYPTION): cv.maybe_simple_value( - cv.Schema( - { - cv.Required(CONF_KEY): cv.string, - } - ), - key=CONF_KEY, - ) -} - -PROVIDER_SCHEMA = cv.Schema( +UDP_SCHEMA = cv.Schema( { - cv.Required(CONF_NAME): cv.valid_name, - } -).extend(ENCRYPTION_SCHEMA) - - -def validate_(config): - if CONF_ENCRYPTION in config: - if CONF_SENSORS not in config and CONF_BINARY_SENSORS not in config: - raise cv.Invalid("No sensors or binary sensors to encrypt") - elif config[CONF_ROLLING_CODE_ENABLE]: - raise cv.Invalid("Rolling code requires an encryption key") - if config[CONF_PING_PONG_ENABLE]: - if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()): - raise cv.Invalid("Ping-pong requires at least one encrypted provider") - return config - - -CONFIG_SCHEMA = cv.All( - cv.polling_component_schema("15s") - .extend( - { - cv.GenerateID(): cv.declare_id(UDPComponent), - cv.Optional(CONF_PORT, default=18511): cv.port, - cv.Optional( - CONF_LISTEN_ADDRESS, default="255.255.255.255" - ): cv.ipv4address_multi_broadcast, - cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4address, - ), - cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean, - cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, - cv.Optional( - CONF_PING_PONG_RECYCLE_TIME, default="600s" - ): cv.positive_time_period_seconds, - cv.Optional(CONF_SENSORS): cv.ensure_list(sensor_validation(Sensor)), - cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( - sensor_validation(BinarySensor) - ), - cv.Optional(CONF_PROVIDERS): cv.ensure_list(PROVIDER_SCHEMA), - }, - ) - .extend(ENCRYPTION_SCHEMA), - validate_, -) - -SENSOR_SCHEMA = cv.Schema( - { - cv.Optional(CONF_REMOTE_ID): cv.string_strict, - cv.Required(CONF_PROVIDER): cv.valid_name, cv.GenerateID(CONF_UDP_ID): cv.use_id(UDPComponent), } ) -def require_internal_with_name(config): - if CONF_NAME in config and CONF_INTERNAL not in config: - raise cv.Invalid("Must provide internal: config when using name:") - return config +def is_relocated(option): + def validator(value): + raise cv.Invalid( + f"The '{option}' option should now be configured in the 'packet_transport' component" + ) + + return validator -def hash_encryption_key(config: dict): - return list(hashlib.sha256(config[CONF_KEY].encode()).digest()) +RELOCATED = { + cv.Optional(x): is_relocated(x) + for x in ( + CONF_PROVIDERS, + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, + CONF_ROLLING_CODE_ENABLE, + CONF_SENSORS, + CONF_BINARY_SENSORS, + ) +} + +CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.Any( + cv.port, + cv.Schema( + { + cv.Required(CONF_LISTEN_PORT): cv.port, + cv.Required(CONF_BROADCAST_PORT): cv.port, + } + ), + ), + cv.Optional( + CONF_LISTEN_ADDRESS, default="255.255.255.255" + ): cv.ipv4address_multi_broadcast, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4address, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(trigger_args) + ), + } + ), + } +).extend(RELOCATED) + + +async def register_udp_client(var, config): + udp_var = await cg.get_variable(config[CONF_UDP_ID]) + cg.add(var.set_parent(udp_var)) + return udp_var async def to_code(config): cg.add_define("USE_UDP") cg.add_global(udp_ns.using) var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - cg.add(var.set_port(config[CONF_PORT])) - cg.add(var.set_rolling_code_enable(config[CONF_ROLLING_CODE_ENABLE])) - cg.add(var.set_ping_pong_enable(config[CONF_PING_PONG_ENABLE])) - cg.add( - var.set_ping_pong_recycle_time( - config[CONF_PING_PONG_RECYCLE_TIME].total_seconds - ) - ) - for sens_conf in config.get(CONF_SENSORS, ()): - sens_id = sens_conf[CONF_ID] - sensor = await cg.get_variable(sens_id) - bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) - cg.add(var.add_sensor(bcst_id, sensor)) - for sens_conf in config.get(CONF_BINARY_SENSORS, ()): - sens_id = sens_conf[CONF_ID] - sensor = await cg.get_variable(sens_id) - bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) - cg.add(var.add_binary_sensor(bcst_id, sensor)) + var = await cg.register_component(var, config) + conf_port = config[CONF_PORT] + if isinstance(conf_port, int): + cg.add(var.set_listen_port(conf_port)) + cg.add(var.set_broadcast_port(conf_port)) + else: + cg.add(var.set_listen_port(conf_port[CONF_LISTEN_PORT])) + cg.add(var.set_broadcast_port(conf_port[CONF_BROADCAST_PORT])) + if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255": + cg.add(var.set_listen_address(listen_address)) for address in config[CONF_ADDRESSES]: cg.add(var.add_address(str(address))) + if on_receive := config.get(CONF_ON_RECEIVE): + on_receive = on_receive[0] + trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) + trigger = await automation.build_automation( + trigger, [(trigger_args, "data")], on_receive + ) + trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data"))))) + trigger = await cg.process_lambda(trigger, [(trigger_args, "data")]) + cg.add(var.add_listener(trigger)) + cg.add(var.set_should_listen()) - if encryption := config.get(CONF_ENCRYPTION): - cg.add(var.set_encryption_key(hash_encryption_key(encryption))) - for provider in config.get(CONF_PROVIDERS, ()): - name = provider[CONF_NAME] - cg.add(var.add_provider(name)) - if (listen_address := str(config[CONF_LISTEN_ADDRESS])) != "255.255.255.255": - cg.add(var.set_listen_address(listen_address)) - if encryption := provider.get(CONF_ENCRYPTION): - cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, str): + return value + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +@automation.register_action( + "udp.write", + UDPWriteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(UDPComponent), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, + ), +) +async def udp_write_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + udp_var = await cg.get_variable(config[CONF_ID]) + await cg.register_parented(var, udp_var) + cg.add(udp_var.set_should_broadcast()) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + return var diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h new file mode 100644 index 0000000000..663daa1c15 --- /dev/null +++ b/esphome/components/udp/automation.h @@ -0,0 +1,38 @@ +#pragma once + +#include "udp_component.h" +#include "esphome/core/automation.h" + +#include + +namespace esphome { +namespace udp { + +template class UDPWriteAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void play(Ts... x) override { + if (this->static_) { + this->parent_->send_packet(this->data_static_); + } else { + auto val = this->data_func_(x...); + this->parent_->send_packet(val); + } + } + + protected: + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +} // namespace udp +} // namespace esphome diff --git a/esphome/components/udp/binary_sensor.py b/esphome/components/udp/binary_sensor.py index d90e495527..7d449efbfd 100644 --- a/esphome/components/udp/binary_sensor.py +++ b/esphome/components/udp/binary_sensor.py @@ -1,27 +1,5 @@ -import esphome.codegen as cg -from esphome.components import binary_sensor -from esphome.config_validation import All, has_at_least_one_key -from esphome.const import CONF_ID +import esphome.config_validation as cv -from . import ( - CONF_PROVIDER, - CONF_REMOTE_ID, - CONF_UDP_ID, - SENSOR_SCHEMA, - require_internal_with_name, +CONFIG_SCHEMA = cv.invalid( + "The 'udp.binary_sensor' component has been migrated to the 'packet_transport.binary_sensor' component." ) - -DEPENDENCIES = ["udp"] - -CONFIG_SCHEMA = All( - binary_sensor.binary_sensor_schema().extend(SENSOR_SCHEMA), - has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), - require_internal_with_name, -) - - -async def to_code(config): - var = await binary_sensor.new_binary_sensor(config) - comp = await cg.get_variable(config[CONF_UDP_ID]) - remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) - cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/packet_transport/__init__.py b/esphome/components/udp/packet_transport/__init__.py new file mode 100644 index 0000000000..b6957a372b --- /dev/null +++ b/esphome/components/udp/packet_transport/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome.components.api import CONF_ENCRYPTION +from esphome.components.packet_transport import ( + CONF_PING_PONG_ENABLE, + PacketTransport, + new_packet_transport, + transport_schema, +) +from esphome.const import CONF_BINARY_SENSORS, CONF_SENSORS +from esphome.cpp_types import PollingComponent + +from .. import UDP_SCHEMA, register_udp_client, udp_ns + +UDPTransport = udp_ns.class_("UDPTransport", PacketTransport, PollingComponent) + +CONFIG_SCHEMA = transport_schema(UDPTransport).extend(UDP_SCHEMA) + + +async def to_code(config): + var, providers = await new_packet_transport(config) + udp_var = await register_udp_client(var, config) + if CONF_ENCRYPTION in config or providers: + cg.add(udp_var.set_should_listen()) + if ( + config[CONF_PING_PONG_ENABLE] + or config.get(CONF_SENSORS, ()) + or config.get(CONF_BINARY_SENSORS, ()) + ): + cg.add(udp_var.set_should_broadcast()) diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp new file mode 100644 index 0000000000..3918760627 --- /dev/null +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -0,0 +1,36 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/network/util.h" +#include "udp_transport.h" + +namespace esphome { +namespace udp { + +static const char *const TAG = "udp_transport"; + +bool UDPTransport::should_send() { return this->should_broadcast_ && network::is_connected(); } +void UDPTransport::setup() { + PacketTransport::setup(); + this->should_broadcast_ = this->ping_pong_enable_; +#ifdef USE_SENSOR + this->should_broadcast_ |= !this->sensors_.empty(); +#endif +#ifdef USE_BINARY_SENSOR + this->should_broadcast_ |= !this->binary_sensors_.empty(); +#endif + if (this->should_broadcast_) + this->parent_->set_should_broadcast(); + if (!this->providers_.empty() || this->is_encrypted_()) { + this->parent_->add_listener([this](std::vector &buf) { this->process_(buf); }); + } +} + +void UDPTransport::update() { + PacketTransport::update(); + this->updated_ = true; + this->resend_data_ = this->should_broadcast_; +} + +void UDPTransport::send_packet(std::vector &buf) const { this->parent_->send_packet(buf); } +} // namespace udp +} // namespace esphome diff --git a/esphome/components/udp/packet_transport/udp_transport.h b/esphome/components/udp/packet_transport/udp_transport.h new file mode 100644 index 0000000000..5a27bc32c7 --- /dev/null +++ b/esphome/components/udp/packet_transport/udp_transport.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../udp_component.h" +#include "esphome/core/component.h" +#include "esphome/components/packet_transport/packet_transport.h" +#include + +namespace esphome { +namespace udp { + +class UDPTransport : public packet_transport::PacketTransport, public Parented { + public: + void setup() override; + void update() override; + + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + void send_packet(std::vector &buf) const override; + bool should_send() override; + bool should_broadcast_{false}; + size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } +}; + +} // namespace udp +} // namespace esphome diff --git a/esphome/components/udp/sensor.py b/esphome/components/udp/sensor.py index 860c277c44..9ce05e7ffb 100644 --- a/esphome/components/udp/sensor.py +++ b/esphome/components/udp/sensor.py @@ -1,27 +1,5 @@ -import esphome.codegen as cg -from esphome.components.sensor import new_sensor, sensor_schema -from esphome.config_validation import All, has_at_least_one_key -from esphome.const import CONF_ID +import esphome.config_validation as cv -from . import ( - CONF_PROVIDER, - CONF_REMOTE_ID, - CONF_UDP_ID, - SENSOR_SCHEMA, - require_internal_with_name, +CONFIG_SCHEMA = cv.invalid( + "The 'udp.sensor' component has been migrated to the 'packet_transport.sensor' component." ) - -DEPENDENCIES = ["udp"] - -CONFIG_SCHEMA = All( - sensor_schema().extend(SENSOR_SCHEMA), - has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), - require_internal_with_name, -) - - -async def to_code(config): - var = await new_sensor(config) - comp = await cg.get_variable(config[CONF_UDP_ID]) - remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) - cg.add(comp.add_remote_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 59cba8c7fe..222c73f82e 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -1,164 +1,24 @@ +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/network/util.h" #include "udp_component.h" -#include "esphome/components/xxtea/xxtea.h" - namespace esphome { namespace udp { -/** - * Structure of a data packet; everything is little-endian - * - * --- In clear text --- - * MAGIC_NUMBER: 16 bits - * host name length: 1 byte - * host name: (length) bytes - * padding: 0 or more null bytes to a 4 byte boundary - * - * --- Encrypted (if key set) ---- - * DATA_KEY: 1 byte: OR ROLLING_CODE_KEY: - * Rolling code (if enabled): 8 bytes - * Ping keys: if any - * repeat: - * PING_KEY: 1 byte - * ping code: 4 bytes - * Sensors: - * repeat: - * SENSOR_KEY: 1 byte - * float value: 4 bytes - * name length: 1 byte - * name - * Binary Sensors: - * repeat: - * BINARY_SENSOR_KEY: 1 byte - * bool value: 1 bytes - * name length: 1 byte - * name - * - * Padded to a 4 byte boundary with nulls - * - * Structure of a ping request packet: - * --- In clear text --- - * MAGIC_PING: 16 bits - * host name length: 1 byte - * host name: (length) bytes - * Ping key (4 bytes) - * - */ static const char *const TAG = "udp"; -static size_t round4(size_t value) { return (value + 3) & ~3; } - -union FuData { - uint32_t u32; - float f32; -}; - -static const size_t MAX_PACKET_SIZE = 508; -static const uint16_t MAGIC_NUMBER = 0x4553; -static const uint16_t MAGIC_PING = 0x5048; -static const uint32_t PREF_HASH = 0x45535043; -enum DataKey { - ZERO_FILL_KEY, - DATA_KEY, - SENSOR_KEY, - BINARY_SENSOR_KEY, - PING_KEY, - ROLLING_CODE_KEY, -}; - -static const size_t MAX_PING_KEYS = 4; - -static inline void add(std::vector &vec, uint32_t data) { - vec.push_back(data & 0xFF); - vec.push_back((data >> 8) & 0xFF); - vec.push_back((data >> 16) & 0xFF); - vec.push_back((data >> 24) & 0xFF); -} - -static inline uint32_t get_uint32(uint8_t *&buf) { - uint32_t data = *buf++; - data += *buf++ << 8; - data += *buf++ << 16; - data += *buf++ << 24; - return data; -} - -static inline uint16_t get_uint16(uint8_t *&buf) { - uint16_t data = *buf++; - data += *buf++ << 8; - return data; -} - -static inline void add(std::vector &vec, uint8_t data) { vec.push_back(data); } -static inline void add(std::vector &vec, uint16_t data) { - vec.push_back((uint8_t) data); - vec.push_back((uint8_t) (data >> 8)); -} -static inline void add(std::vector &vec, DataKey data) { vec.push_back(data); } -static void add(std::vector &vec, const char *str) { - auto len = strlen(str); - vec.push_back(len); - for (size_t i = 0; i != len; i++) { - vec.push_back(*str++); - } -} - void UDPComponent::setup() { - this->name_ = App.get_name().c_str(); - if (strlen(this->name_) > 255) { - this->mark_failed(); - this->status_set_error("Device name exceeds 255 chars"); - return; - } - this->resend_ping_key_ = this->ping_pong_enable_; - // restore the upper 32 bits of the rolling code, increment and save. - this->pref_ = global_preferences->make_preference(PREF_HASH, true); - this->pref_.load(&this->rolling_code_[1]); - this->rolling_code_[1]++; - this->pref_.save(&this->rolling_code_[1]); - this->ping_key_ = random_uint32(); - ESP_LOGV(TAG, "Rolling code incremented, upper part now %u", (unsigned) this->rolling_code_[1]); -#ifdef USE_SENSOR - for (auto &sensor : this->sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](float x) { - this->updated_ = true; - sensor.updated = true; - }); - } -#endif -#ifdef USE_BINARY_SENSOR - for (auto &sensor : this->binary_sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](bool value) { - this->updated_ = true; - sensor.updated = true; - }); - } -#endif - this->should_send_ = this->ping_pong_enable_; -#ifdef USE_SENSOR - this->should_send_ |= !this->sensors_.empty(); -#endif -#ifdef USE_BINARY_SENSOR - this->should_send_ |= !this->binary_sensors_.empty(); -#endif - this->should_listen_ = !this->providers_.empty() || this->is_encrypted_(); - // initialise the header. This is invariant. - add(this->header_, MAGIC_NUMBER); - add(this->header_, this->name_); - // pad to a multiple of 4 bytes - while (this->header_.size() & 0x3) - this->header_.push_back(0); #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) for (const auto &address : this->addresses_) { struct sockaddr saddr {}; - socket::set_sockaddr(&saddr, sizeof(saddr), address, this->port_); + socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_); this->sockaddrs_.push_back(saddr); } // set up broadcast socket - if (this->should_send_) { + if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { this->mark_failed(); @@ -202,14 +62,14 @@ void UDPComponent::setup() { server.sin_family = AF_INET; server.sin_addr.s_addr = ESPHOME_INADDR_ANY; - server.sin_port = htons(this->port_); + server.sin_port = htons(this->listen_port_); if (this->listen_address_.has_value()) { struct ip_mreq imreq = {}; imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY; inet_aton(this->listen_address_.value().str().c_str(), &imreq.imr_multiaddr); server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr; - ESP_LOGV(TAG, "Join multicast %s", this->listen_address_.value().str().c_str()); + ESP_LOGD(TAG, "Join multicast %s", this->listen_address_.value().str().c_str()); err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); @@ -236,341 +96,48 @@ void UDPComponent::setup() { this->ipaddrs_.push_back(ipaddr); } if (this->should_listen_) - this->udp_client_.begin(this->port_); + this->udp_client_.begin(this->listen_port_); #endif } -void UDPComponent::init_data_() { - this->data_.clear(); - if (this->rolling_code_enable_) { - add(this->data_, ROLLING_CODE_KEY); - add(this->data_, this->rolling_code_[0]); - add(this->data_, this->rolling_code_[1]); - this->increment_code_(); - } else { - add(this->data_, DATA_KEY); - } - for (auto pkey : this->ping_keys_) { - add(this->data_, PING_KEY); - add(this->data_, pkey.second); - } -} - -void UDPComponent::flush_() { - if (!network::is_connected() || this->data_.empty()) - return; - uint32_t buffer[MAX_PACKET_SIZE / 4]; - memset(buffer, 0, sizeof buffer); - // len must be a multiple of 4 - auto header_len = round4(this->header_.size()) / 4; - auto len = round4(data_.size()) / 4; - memcpy(buffer, this->header_.data(), this->header_.size()); - memcpy(buffer + header_len, this->data_.data(), this->data_.size()); - if (this->is_encrypted_()) { - xxtea::encrypt(buffer + header_len, len, (uint32_t *) this->encryption_key_.data()); - } - auto total_len = (header_len + len) * 4; - this->send_packet_(buffer, total_len); -} - -void UDPComponent::add_binary_data_(uint8_t key, const char *id, bool data) { - auto len = 1 + 1 + 1 + strlen(id); - if (len + this->header_.size() + this->data_.size() > MAX_PACKET_SIZE) { - this->flush_(); - } - add(this->data_, key); - add(this->data_, (uint8_t) data); - add(this->data_, id); -} -void UDPComponent::add_data_(uint8_t key, const char *id, float data) { - FuData udata{.f32 = data}; - this->add_data_(key, id, udata.u32); -} - -void UDPComponent::add_data_(uint8_t key, const char *id, uint32_t data) { - auto len = 4 + 1 + 1 + strlen(id); - if (len + this->header_.size() + this->data_.size() > MAX_PACKET_SIZE) { - this->flush_(); - } - add(this->data_, key); - add(this->data_, data); - add(this->data_, id); -} -void UDPComponent::send_data_(bool all) { - if (!this->should_send_ || !network::is_connected()) - return; - this->init_data_(); -#ifdef USE_SENSOR - for (auto &sensor : this->sensors_) { - if (all || sensor.updated) { - sensor.updated = false; - this->add_data_(SENSOR_KEY, sensor.id, sensor.sensor->get_state()); - } - } -#endif -#ifdef USE_BINARY_SENSOR - for (auto &sensor : this->binary_sensors_) { - if (all || sensor.updated) { - sensor.updated = false; - this->add_binary_data_(BINARY_SENSOR_KEY, sensor.id, sensor.sensor->state); - } - } -#endif - this->flush_(); - this->updated_ = false; - this->resend_data_ = false; -} - -void UDPComponent::update() { - this->updated_ = true; - this->resend_data_ = this->should_send_; - auto now = millis() / 1000; - if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { - this->resend_ping_key_ = this->ping_pong_enable_; - this->last_key_time_ = now; - } -} - void UDPComponent::loop() { - uint8_t buf[MAX_PACKET_SIZE]; + auto buf = std::vector(MAX_PACKET_SIZE); if (this->should_listen_) { for (;;) { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) - auto len = this->listen_socket_->read(buf, sizeof(buf)); + auto len = this->listen_socket_->read(buf.data(), buf.size()); #endif #ifdef USE_SOCKET_IMPL_LWIP_TCP auto len = this->udp_client_.parsePacket(); if (len > 0) - len = this->udp_client_.read(buf, sizeof(buf)); + len = this->udp_client_.read(buf.data(), buf.size()); #endif - if (len > 0) { - this->process_(buf, len); - continue; - } - break; + if (len <= 0) + break; + buf.resize(len); + ESP_LOGV(TAG, "Received packet of length %zu", len); + this->packet_listeners_.call(buf); } } - if (this->resend_ping_key_) - this->send_ping_pong_request_(); - if (this->updated_) { - this->send_data_(this->resend_data_); - } -} - -void UDPComponent::add_key_(const char *name, uint32_t key) { - if (!this->is_encrypted_()) - return; - if (this->ping_keys_.count(name) == 0 && this->ping_keys_.size() == MAX_PING_KEYS) { - ESP_LOGW(TAG, "Ping key from %s discarded", name); - return; - } - this->ping_keys_[name] = key; - this->resend_data_ = true; - ESP_LOGV(TAG, "Ping key from %s now %X", name, (unsigned) key); -} - -void UDPComponent::process_ping_request_(const char *name, uint8_t *ptr, size_t len) { - if (len != 4) { - ESP_LOGW(TAG, "Bad ping request"); - return; - } - auto key = get_uint32(ptr); - this->add_key_(name, key); - ESP_LOGV(TAG, "Updated ping key for %s to %08X", name, (unsigned) key); -} - -static bool process_rolling_code(Provider &provider, uint8_t *&buf, const uint8_t *end) { - if (end - buf < 8) - return false; - auto code0 = get_uint32(buf); - auto code1 = get_uint32(buf); - if (code1 < provider.last_code[1] || (code1 == provider.last_code[1] && code0 <= provider.last_code[0])) { - ESP_LOGW(TAG, "Rolling code for %s %08lX:%08lX is old", provider.name, (unsigned long) code1, - (unsigned long) code0); - return false; - } - provider.last_code[0] = code0; - provider.last_code[1] = code1; - return true; -} - -/** - * Process a received packet - */ -void UDPComponent::process_(uint8_t *buf, const size_t len) { - auto ping_key_seen = !this->ping_pong_enable_; - if (len < 8) { - ESP_LOGV(TAG, "Bad length %zu", len); - return; - } - char namebuf[256]{}; - uint8_t byte; - uint8_t *start_ptr = buf; - const uint8_t *end = buf + len; - FuData rdata{}; - auto magic = get_uint16(buf); - if (magic != MAGIC_NUMBER && magic != MAGIC_PING) { - ESP_LOGV(TAG, "Bad magic %X", magic); - return; - } - - auto hlen = *buf++; - if (hlen > len - 3) { - ESP_LOGV(TAG, "Bad hostname length %u > %zu", hlen, len - 3); - return; - } - memcpy(namebuf, buf, hlen); - if (strcmp(this->name_, namebuf) == 0) { - ESP_LOGV(TAG, "Ignoring our own data"); - return; - } - buf += hlen; - if (magic == MAGIC_PING) { - this->process_ping_request_(namebuf, buf, end - buf); - return; - } - if (round4(len) != len) { - ESP_LOGW(TAG, "Bad length %zu", len); - return; - } - hlen = round4(hlen + 3); - buf = start_ptr + hlen; - if (buf == end) { - ESP_LOGV(TAG, "No data after header"); - return; - } - - if (this->providers_.count(namebuf) == 0) { - ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); - return; - } - auto &provider = this->providers_[namebuf]; - // if encryption not used with this host, ping check is pointless since it would be easily spoofed. - if (provider.encryption_key.empty()) - ping_key_seen = true; - - ESP_LOGV(TAG, "Found hostname %s", namebuf); -#ifdef USE_SENSOR - auto &sensors = this->remote_sensors_[namebuf]; -#endif -#ifdef USE_BINARY_SENSOR - auto &binary_sensors = this->remote_binary_sensors_[namebuf]; -#endif - - if (!provider.encryption_key.empty()) { - xxtea::decrypt((uint32_t *) buf, (end - buf) / 4, (uint32_t *) provider.encryption_key.data()); - } - byte = *buf++; - if (byte == ROLLING_CODE_KEY) { - if (!process_rolling_code(provider, buf, end)) - return; - } else if (byte != DATA_KEY) { - ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte); - return; - } - while (buf < end) { - byte = *buf++; - if (byte == ZERO_FILL_KEY) - continue; - if (byte == PING_KEY) { - if (end - buf < 4) { - ESP_LOGV(TAG, "PING_KEY requires 4 more bytes"); - return; - } - auto key = get_uint32(buf); - if (key == this->ping_key_) { - ping_key_seen = true; - ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key); - } else { - ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); - } - continue; - } - if (!ping_key_seen) { - ESP_LOGW(TAG, "Ping key not seen"); - this->resend_ping_key_ = true; - break; - } - if (byte == BINARY_SENSOR_KEY) { - if (end - buf < 3) { - ESP_LOGV(TAG, "Binary sensor key requires at least 3 more bytes"); - return; - } - rdata.u32 = *buf++; - } else if (byte == SENSOR_KEY) { - if (end - buf < 6) { - ESP_LOGV(TAG, "Sensor key requires at least 6 more bytes"); - return; - } - rdata.u32 = get_uint32(buf); - } else { - ESP_LOGW(TAG, "Unknown key byte %X", byte); - return; - } - - hlen = *buf++; - if (end - buf < hlen) { - ESP_LOGV(TAG, "Name length of %u not available", hlen); - return; - } - memset(namebuf, 0, sizeof namebuf); - memcpy(namebuf, buf, hlen); - ESP_LOGV(TAG, "Found sensor key %d, id %s, data %lX", byte, namebuf, (unsigned long) rdata.u32); - buf += hlen; -#ifdef USE_SENSOR - if (byte == SENSOR_KEY && sensors.count(namebuf) != 0) - sensors[namebuf]->publish_state(rdata.f32); -#endif -#ifdef USE_BINARY_SENSOR - if (byte == BINARY_SENSOR_KEY && binary_sensors.count(namebuf) != 0) - binary_sensors[namebuf]->publish_state(rdata.u32 != 0); -#endif - } } void UDPComponent::dump_config() { ESP_LOGCONFIG(TAG, "UDP:"); - ESP_LOGCONFIG(TAG, " Port: %u", this->port_); - ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(this->is_encrypted_())); - ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); + ESP_LOGCONFIG(TAG, " Listen Port: %u", this->listen_port_); + ESP_LOGCONFIG(TAG, " Broadcast Port: %u", this->broadcast_port_); for (const auto &address : this->addresses_) ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); if (this->listen_address_.has_value()) { ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str()); } -#ifdef USE_SENSOR - for (auto sensor : this->sensors_) - ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); -#endif -#ifdef USE_BINARY_SENSOR - for (auto sensor : this->binary_sensors_) - ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); -#endif - for (const auto &host : this->providers_) { - ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); - ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(!host.second.encryption_key.empty())); -#ifdef USE_SENSOR - for (const auto &sensor : this->remote_sensors_[host.first.c_str()]) - ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.first.c_str()); -#endif -#ifdef USE_BINARY_SENSOR - for (const auto &sensor : this->remote_binary_sensors_[host.first.c_str()]) - ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.first.c_str()); -#endif - } + ESP_LOGCONFIG(TAG, " Broadcasting: %s", YESNO(this->should_broadcast_)); + ESP_LOGCONFIG(TAG, " Listening: %s", YESNO(this->should_listen_)); } -void UDPComponent::increment_code_() { - if (this->rolling_code_enable_) { - if (++this->rolling_code_[0] == 0) { - this->rolling_code_[1]++; - this->pref_.save(&this->rolling_code_[1]); - } - } -} -void UDPComponent::send_packet_(void *data, size_t len) { + +void UDPComponent::send_packet(const uint8_t *data, size_t size) { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) for (const auto &saddr : this->sockaddrs_) { - auto result = this->broadcast_socket_->sendto(data, len, 0, &saddr, sizeof(saddr)); + auto result = this->broadcast_socket_->sendto(data, size, 0, &saddr, sizeof(saddr)); if (result < 0) ESP_LOGW(TAG, "sendto() error %d", errno); } @@ -578,8 +145,8 @@ void UDPComponent::send_packet_(void *data, size_t len) { #ifdef USE_SOCKET_IMPL_LWIP_TCP auto iface = IPAddress(0, 0, 0, 0); for (const auto &saddr : this->ipaddrs_) { - if (this->udp_client_.beginPacketMulticast(saddr, this->port_, iface, 128) != 0) { - this->udp_client_.write((const uint8_t *) data, len); + if (this->udp_client_.beginPacketMulticast(saddr, this->broadcast_port_, iface, 128) != 0) { + this->udp_client_.write(data, size); auto result = this->udp_client_.endPacket(); if (result == 0) ESP_LOGW(TAG, "udp.write() error"); @@ -587,18 +154,7 @@ void UDPComponent::send_packet_(void *data, size_t len) { } #endif } - -void UDPComponent::send_ping_pong_request_() { - if (!this->ping_pong_enable_ || !network::is_connected()) - return; - this->ping_key_ = random_uint32(); - this->ping_header_.clear(); - add(this->ping_header_, MAGIC_PING); - add(this->ping_header_, this->name_); - add(this->ping_header_, this->ping_key_); - this->send_packet_(this->ping_header_.data(), this->ping_header_.size()); - this->resend_ping_key_ = false; - ESP_LOGV(TAG, "Sent new ping request %08X", (unsigned) this->ping_key_); -} } // namespace udp } // namespace esphome + +#endif diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h index 02f998ded7..25909eba1d 100644 --- a/esphome/components/udp/udp_component.h +++ b/esphome/components/udp/udp_component.h @@ -1,13 +1,8 @@ #pragma once -#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/network/ip_address.h" -#ifdef USE_SENSOR -#include "esphome/components/sensor/sensor.h" -#endif -#ifdef USE_BINARY_SENSOR -#include "esphome/components/binary_sensor/binary_sensor.h" -#endif #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" #endif @@ -15,116 +10,35 @@ #include #endif #include -#include namespace esphome { namespace udp { -struct Provider { - std::vector encryption_key; - const char *name; - uint32_t last_code[2]; -}; - -#ifdef USE_SENSOR -struct Sensor { - sensor::Sensor *sensor; - const char *id; - bool updated; -}; -#endif -#ifdef USE_BINARY_SENSOR -struct BinarySensor { - binary_sensor::BinarySensor *sensor; - const char *id; - bool updated; -}; -#endif - -class UDPComponent : public PollingComponent { +static const size_t MAX_PACKET_SIZE = 508; +class UDPComponent : public Component { public: + void add_address(const char *addr) { this->addresses_.emplace_back(addr); } + void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); } + void set_listen_port(uint16_t port) { this->listen_port_ = port; } + void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; } + void set_should_broadcast() { this->should_broadcast_ = true; } + void set_should_listen() { this->should_listen_ = true; } + void add_listener(std::function &)> &&listener) { + this->packet_listeners_.add(std::move(listener)); + } void setup() override; void loop() override; - void update() override; void dump_config() override; - -#ifdef USE_SENSOR - void add_sensor(const char *id, sensor::Sensor *sensor) { - Sensor st{sensor, id, true}; - this->sensors_.push_back(st); - } - void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { - this->add_provider(hostname); - this->remote_sensors_[hostname][remote_id] = sensor; - } -#endif -#ifdef USE_BINARY_SENSOR - void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { - BinarySensor st{sensor, id, true}; - this->binary_sensors_.push_back(st); - } - - void add_remote_binary_sensor(const char *hostname, const char *remote_id, binary_sensor::BinarySensor *sensor) { - this->add_provider(hostname); - this->remote_binary_sensors_[hostname][remote_id] = sensor; - } -#endif - void add_address(const char *addr) { this->addresses_.emplace_back(addr); } -#ifdef USE_NETWORK - void set_listen_address(const char *listen_addr) { this->listen_address_ = network::IPAddress(listen_addr); } -#endif - void set_port(uint16_t port) { this->port_ = port; } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - - void add_provider(const char *hostname) { - if (this->providers_.count(hostname) == 0) { - Provider provider; - provider.encryption_key = std::vector{}; - provider.last_code[0] = 0; - provider.last_code[1] = 0; - provider.name = hostname; - this->providers_[hostname] = provider; -#ifdef USE_SENSOR - this->remote_sensors_[hostname] = std::map(); -#endif -#ifdef USE_BINARY_SENSOR - this->remote_binary_sensors_[hostname] = std::map(); -#endif - } - } - - void set_encryption_key(std::vector key) { this->encryption_key_ = std::move(key); } - void set_rolling_code_enable(bool enable) { this->rolling_code_enable_ = enable; } - void set_ping_pong_enable(bool enable) { this->ping_pong_enable_ = enable; } - void set_ping_pong_recycle_time(uint32_t recycle_time) { this->ping_pong_recyle_time_ = recycle_time; } - void set_provider_encryption(const char *name, std::vector key) { - this->providers_[name].encryption_key = std::move(key); - } + void send_packet(const uint8_t *data, size_t size); + void send_packet(std::vector &buf) { this->send_packet(buf.data(), buf.size()); } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }; protected: - void send_data_(bool all); - void process_(uint8_t *buf, size_t len); - void flush_(); - void add_data_(uint8_t key, const char *id, float data); - void add_data_(uint8_t key, const char *id, uint32_t data); - void increment_code_(); - void add_binary_data_(uint8_t key, const char *id, bool data); - void init_data_(); - - bool updated_{}; - uint16_t port_{18511}; - uint32_t ping_key_{}; - uint32_t rolling_code_[2]{}; - bool rolling_code_enable_{}; - bool ping_pong_enable_{}; - uint32_t ping_pong_recyle_time_{}; - uint32_t last_key_time_{}; - bool resend_ping_key_{}; - bool resend_data_{}; - bool should_send_{}; - const char *name_{}; + uint16_t listen_port_{}; + uint16_t broadcast_port_{}; + bool should_broadcast_{}; bool should_listen_{}; - ESPPreferenceObject pref_; + CallbackManager &)> packet_listeners_{}; #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr broadcast_socket_ = nullptr; @@ -135,32 +49,11 @@ class UDPComponent : public PollingComponent { std::vector ipaddrs_{}; WiFiUDP udp_client_{}; #endif - std::vector encryption_key_{}; std::vector addresses_{}; -#ifdef USE_SENSOR - std::vector sensors_{}; - std::map> remote_sensors_{}; -#endif -#ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; - std::map> remote_binary_sensors_{}; -#endif -#ifdef USE_NETWORK optional listen_address_{}; -#endif - std::map providers_{}; - std::vector ping_header_{}; - std::vector header_{}; - std::vector data_{}; - std::map ping_keys_{}; - void add_key_(const char *name, uint32_t key); - void send_ping_pong_request_(); - void send_packet_(void *data, size_t len); - void process_ping_request_(const char *name, uint8_t *ptr, size_t len); - - inline bool is_encrypted_() { return !this->encryption_key_.empty(); } }; } // namespace udp } // namespace esphome +#endif diff --git a/tests/components/packet_transport/common.yaml b/tests/components/packet_transport/common.yaml new file mode 100644 index 0000000000..cbb34c4572 --- /dev/null +++ b/tests/components/packet_transport/common.yaml @@ -0,0 +1,40 @@ +wifi: + ssid: MySSID + password: password1 + +udp: + listen_address: 239.0.60.53 + addresses: ["239.0.60.53"] + +packet_transport: + platform: udp + update_interval: 5s + encryption: "our key goes here" + rolling_code_enable: true + ping_pong_enable: true + binary_sensors: + - binary_sensor_id1 + - id: binary_sensor_id1 + broadcast_id: other_id + sensors: + - sensor_id1 + - id: sensor_id1 + broadcast_id: other_id + providers: + - name: some-device-name + encryption: "their key goes here" + +sensor: + - platform: template + id: sensor_id1 + - platform: packet_transport + provider: some-device-name + id: our_id + remote_id: some_sensor_id + +binary_sensor: + - platform: packet_transport + provider: unencrypted-device + id: other_binary_sensor_id + - platform: template + id: binary_sensor_id1 diff --git a/tests/components/packet_transport/test.bk72xx-ard.yaml b/tests/components/packet_transport/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-ard.yaml b/tests/components/packet_transport/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-c3-ard.yaml b/tests/components/packet_transport/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-c3-idf.yaml b/tests/components/packet_transport/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-idf.yaml b/tests/components/packet_transport/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp8266-ard.yaml b/tests/components/packet_transport/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/packet_transport/test.host.yaml b/tests/components/packet_transport/test.host.yaml new file mode 100644 index 0000000000..e735c37e4d --- /dev/null +++ b/tests/components/packet_transport/test.host.yaml @@ -0,0 +1,4 @@ +packages: + common: !include common.yaml + +wifi: !remove diff --git a/tests/components/packet_transport/test.rp2040-ard.yaml b/tests/components/packet_transport/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/packet_transport/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index e533cb965e..79da02a692 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -3,34 +3,18 @@ wifi: password: password1 udp: - update_interval: 5s - encryption: "our key goes here" - rolling_code_enable: true - ping_pong_enable: true + id: my_udp listen_address: 239.0.60.53 - binary_sensors: - - binary_sensor_id1 - - id: binary_sensor_id1 - broadcast_id: other_id - sensors: - - sensor_id1 - - id: sensor_id1 - broadcast_id: other_id - providers: - - name: some-device-name - encryption: "their key goes here" + addresses: ["239.0.60.53"] + on_receive: + - logger.log: + format: "Received %d bytes" + args: [data.size()] + - udp.write: + id: my_udp + data: "hello world" + - udp.write: + id: my_udp + data: !lambda |- + return std::vector{1,3,4,5,6}; -sensor: - - platform: template - id: sensor_id1 - - platform: udp - provider: some-device-name - id: our_id - remote_id: some_sensor_id - -binary_sensor: - - platform: udp - provider: unencrypted-device - id: other_binary_sensor_id - - platform: template - id: binary_sensor_id1