mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	[udp] Implement UDP sensor broadcast (#6865)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: clydebarrow <366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
		| @@ -423,6 +423,7 @@ esphome/components/tuya/switch/* @jesserockz | |||||||
| esphome/components/tuya/text_sensor/* @dentra | esphome/components/tuya/text_sensor/* @dentra | ||||||
| esphome/components/uart/* @esphome/core | esphome/components/uart/* @esphome/core | ||||||
| esphome/components/uart/button/* @ssieb | esphome/components/uart/button/* @ssieb | ||||||
|  | esphome/components/udp/* @clydebarrow | ||||||
| esphome/components/ufire_ec/* @pvizeli | esphome/components/ufire_ec/* @pvizeli | ||||||
| esphome/components/ufire_ise/* @pvizeli | esphome/components/ufire_ise/* @pvizeli | ||||||
| esphome/components/ultrasonic/* @OttoWinter | esphome/components/ultrasonic/* @OttoWinter | ||||||
|   | |||||||
							
								
								
									
										158
									
								
								esphome/components/udp/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								esphome/components/udp/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | import hashlib | ||||||
|  |  | ||||||
|  | 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_PORT, | ||||||
|  |     CONF_SENSORS, | ||||||
|  | ) | ||||||
|  | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@clydebarrow"] | ||||||
|  | DEPENDENCIES = ["network"] | ||||||
|  | AUTO_LOAD = ["socket"] | ||||||
|  | MULTI_CONF = True | ||||||
|  |  | ||||||
|  | udp_ns = cg.esphome_ns.namespace("udp") | ||||||
|  | UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent) | ||||||
|  |  | ||||||
|  | CONF_BROADCAST = "broadcast" | ||||||
|  | CONF_BROADCAST_ID = "broadcast_id" | ||||||
|  | CONF_ADDRESSES = "addresses" | ||||||
|  | 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" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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( | ||||||
|  |     { | ||||||
|  |         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_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( | ||||||
|  |                 cv.ipv4 | ||||||
|  |             ), | ||||||
|  |             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 hash_encryption_key(config: dict): | ||||||
|  |     return list(hashlib.sha256(config[CONF_KEY].encode()).digest()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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)) | ||||||
|  |     for address in config[CONF_ADDRESSES]: | ||||||
|  |         cg.add(var.add_address(str(address))) | ||||||
|  |  | ||||||
|  |     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 encryption := provider.get(CONF_ENCRYPTION): | ||||||
|  |             cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) | ||||||
							
								
								
									
										27
									
								
								esphome/components/udp/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/udp/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | 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 | ||||||
|  |  | ||||||
|  | from . import ( | ||||||
|  |     CONF_PROVIDER, | ||||||
|  |     CONF_REMOTE_ID, | ||||||
|  |     CONF_UDP_ID, | ||||||
|  |     SENSOR_SCHEMA, | ||||||
|  |     require_internal_with_name, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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)) | ||||||
							
								
								
									
										27
									
								
								esphome/components/udp/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/udp/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | 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 | ||||||
|  |  | ||||||
|  | from . import ( | ||||||
|  |     CONF_PROVIDER, | ||||||
|  |     CONF_REMOTE_ID, | ||||||
|  |     CONF_UDP_ID, | ||||||
|  |     SENSOR_SCHEMA, | ||||||
|  |     require_internal_with_name, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | 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)) | ||||||
							
								
								
									
										616
									
								
								esphome/components/udp/udp_component.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										616
									
								
								esphome/components/udp/udp_component.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,616 @@ | |||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "esphome/components/network/util.h" | ||||||
|  | #include "udp_component.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"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * XXTEA implementation, using 256 bit key. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | static const uint32_t DELTA = 0x9e3779b9; | ||||||
|  | #define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Encrypt a block of data in-place | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | static void xxtea_encrypt(uint32_t *v, size_t n, const uint32_t *k) { | ||||||
|  |   uint32_t z, y, sum, e; | ||||||
|  |   size_t p; | ||||||
|  |   size_t q = 6 + 52 / n; | ||||||
|  |   sum = 0; | ||||||
|  |   z = v[n - 1]; | ||||||
|  |   while (q-- != 0) { | ||||||
|  |     sum += DELTA; | ||||||
|  |     e = (sum >> 2); | ||||||
|  |     for (p = 0; p != n - 1; p++) { | ||||||
|  |       y = v[p + 1]; | ||||||
|  |       z = v[p] += MX; | ||||||
|  |     } | ||||||
|  |     y = v[0]; | ||||||
|  |     z = v[n - 1] += MX; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static void xxtea_decrypt(uint32_t *v, size_t n, const uint32_t *k) { | ||||||
|  |   uint32_t z, y, sum, e; | ||||||
|  |   size_t p; | ||||||
|  |   size_t q = 6 + 52 / n; | ||||||
|  |   sum = q * DELTA; | ||||||
|  |   y = v[0]; | ||||||
|  |   while (q-- != 0) { | ||||||
|  |     e = (sum >> 2); | ||||||
|  |     for (p = n - 1; p != 0; p--) { | ||||||
|  |       z = v[p - 1]; | ||||||
|  |       y = v[p] -= MX; | ||||||
|  |     } | ||||||
|  |     z = v[n - 1]; | ||||||
|  |     y = v[0] -= MX; | ||||||
|  |     sum -= DELTA; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline 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<uint8_t> &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<uint8_t> &vec, uint8_t data) { vec.push_back(data); } | ||||||
|  | static inline void add(std::vector<uint8_t> &vec, uint16_t data) { | ||||||
|  |   vec.push_back((uint8_t) data); | ||||||
|  |   vec.push_back((uint8_t) (data >> 8)); | ||||||
|  | } | ||||||
|  | static inline void add(std::vector<uint8_t> &vec, DataKey data) { vec.push_back(data); } | ||||||
|  | static void add(std::vector<uint8_t> &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<uint32_t>(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_); | ||||||
|  |     this->sockaddrs_.push_back(saddr); | ||||||
|  |   } | ||||||
|  |   // set up broadcast socket | ||||||
|  |   if (this->should_send_) { | ||||||
|  |     this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); | ||||||
|  |     if (this->broadcast_socket_ == nullptr) { | ||||||
|  |       this->mark_failed(); | ||||||
|  |       this->status_set_error("Could not create socket"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     int enable = 1; | ||||||
|  |     auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); | ||||||
|  |     if (err != 0) { | ||||||
|  |       this->status_set_warning("Socket unable to set reuseaddr"); | ||||||
|  |       // we can still continue | ||||||
|  |     } | ||||||
|  |     err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); | ||||||
|  |     if (err != 0) { | ||||||
|  |       this->status_set_warning("Socket unable to set broadcast"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // create listening socket if we either want to subscribe to providers, or need to listen | ||||||
|  |   // for ping key broadcasts. | ||||||
|  |   if (this->should_listen_) { | ||||||
|  |     this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); | ||||||
|  |     if (this->listen_socket_ == nullptr) { | ||||||
|  |       this->mark_failed(); | ||||||
|  |       this->status_set_error("Could not create socket"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     auto err = this->listen_socket_->setblocking(false); | ||||||
|  |     if (err < 0) { | ||||||
|  |       ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       this->status_set_error("Unable to set nonblocking"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     int enable = 1; | ||||||
|  |     err = this->listen_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); | ||||||
|  |     if (err != 0) { | ||||||
|  |       this->status_set_warning("Socket unable to set reuseaddr"); | ||||||
|  |       // we can still continue | ||||||
|  |     } | ||||||
|  |     struct sockaddr_in server {}; | ||||||
|  |  | ||||||
|  |     socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); | ||||||
|  |     if (sl == 0) { | ||||||
|  |       ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       this->status_set_error("Unable to set sockaddr"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); | ||||||
|  |     if (err != 0) { | ||||||
|  |       ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); | ||||||
|  |       this->mark_failed(); | ||||||
|  |       this->status_set_error("Unable to bind socket"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | #else | ||||||
|  |   // 8266 and RP2040 `Duino | ||||||
|  |   for (const auto &address : this->addresses_) { | ||||||
|  |     auto ipaddr = IPAddress(); | ||||||
|  |     ipaddr.fromString(address.c_str()); | ||||||
|  |     this->ipaddrs_.push_back(ipaddr); | ||||||
|  |   } | ||||||
|  |   if (this->should_listen_) | ||||||
|  |     this->udp_client_.begin(this->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]; | ||||||
|  |   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)); | ||||||
|  | #else | ||||||
|  |       auto len = this->udp_client_.parsePacket(); | ||||||
|  |       if (len > 0) | ||||||
|  |         len = this->udp_client_.read(buf, sizeof(buf)); | ||||||
|  | #endif | ||||||
|  |       if (len > 0) { | ||||||
|  |         this->process_(buf, len); | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   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) { | ||||||
|  |     return ESP_LOGV(TAG, "Bad length %zu", len); | ||||||
|  |   } | ||||||
|  |   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) | ||||||
|  |     return ESP_LOGV(TAG, "Bad magic %X", magic); | ||||||
|  |  | ||||||
|  |   auto hlen = *buf++; | ||||||
|  |   if (hlen > len - 3) { | ||||||
|  |     return ESP_LOGV(TAG, "Bad hostname length %u > %zu", hlen, len - 3); | ||||||
|  |   } | ||||||
|  |   memcpy(namebuf, buf, hlen); | ||||||
|  |   if (strcmp(this->name_, namebuf) == 0) { | ||||||
|  |     return ESP_LOGV(TAG, "Ignoring our own data"); | ||||||
|  |   } | ||||||
|  |   buf += hlen; | ||||||
|  |   if (magic == MAGIC_PING) | ||||||
|  |     return this->process_ping_request_(namebuf, buf, end - buf); | ||||||
|  |   if (round4(len) != len) { | ||||||
|  |     return ESP_LOGW(TAG, "Bad length %zu", len); | ||||||
|  |   } | ||||||
|  |   hlen = round4(hlen + 3); | ||||||
|  |   buf = start_ptr + hlen; | ||||||
|  |   if (buf == end) { | ||||||
|  |     return ESP_LOGV(TAG, "No data after header"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->providers_.count(namebuf) == 0) { | ||||||
|  |     return ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); | ||||||
|  |   } | ||||||
|  |   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) { | ||||||
|  |     return ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte); | ||||||
|  |   } | ||||||
|  |   while (buf < end) { | ||||||
|  |     byte = *buf++; | ||||||
|  |     if (byte == ZERO_FILL_KEY) | ||||||
|  |       continue; | ||||||
|  |     if (byte == PING_KEY) { | ||||||
|  |       if (end - buf < 4) { | ||||||
|  |         return ESP_LOGV(TAG, "PING_KEY requires 4 more bytes"); | ||||||
|  |       } | ||||||
|  |       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) { | ||||||
|  |         return ESP_LOGV(TAG, "Binary sensor key requires at least 3 more bytes"); | ||||||
|  |       } | ||||||
|  |       rdata.u32 = *buf++; | ||||||
|  |     } else if (byte == SENSOR_KEY) { | ||||||
|  |       if (end - buf < 6) { | ||||||
|  |         return ESP_LOGV(TAG, "Sensor key requires at least 6 more bytes"); | ||||||
|  |       } | ||||||
|  |       rdata.u32 = get_uint32(buf); | ||||||
|  |     } else { | ||||||
|  |       return ESP_LOGW(TAG, "Unknown key byte %X", byte); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     hlen = *buf++; | ||||||
|  |     if (end - buf < hlen) { | ||||||
|  |       return ESP_LOGV(TAG, "Name length of %u not available", hlen); | ||||||
|  |     } | ||||||
|  |     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_)); | ||||||
|  |   for (const auto &address : this->addresses_) | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Address: %s", address.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 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 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) { | ||||||
|  | #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)); | ||||||
|  |     if (result < 0) | ||||||
|  |       ESP_LOGW(TAG, "sendto() error %d", errno); | ||||||
|  |   } | ||||||
|  | #else | ||||||
|  |   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); | ||||||
|  |       auto result = this->udp_client_.endPacket(); | ||||||
|  |       if (result == 0) | ||||||
|  |         ESP_LOGW(TAG, "udp.write() error"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | #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 | ||||||
							
								
								
									
										158
									
								
								esphome/components/udp/udp_component.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								esphome/components/udp/udp_component.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.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" | ||||||
|  | #else | ||||||
|  | #include <WiFiUdp.h> | ||||||
|  | #endif | ||||||
|  | #include <vector> | ||||||
|  | #include <map> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace udp { | ||||||
|  |  | ||||||
|  | struct Provider { | ||||||
|  |   std::vector<uint8_t> 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 { | ||||||
|  |  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_address(const char *addr) { this->addresses_.emplace_back(addr); } | ||||||
|  |   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<uint8_t>{}; | ||||||
|  |       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<std::string, sensor::Sensor *>(); | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |       this->remote_binary_sensors_[hostname] = std::map<std::string, binary_sensor::BinarySensor *>(); | ||||||
|  | #endif | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void set_encryption_key(std::vector<uint8_t> 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<uint8_t> key) { | ||||||
|  |     this->providers_[name].encryption_key = std::move(key); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  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_{}; | ||||||
|  |   bool should_listen_{}; | ||||||
|  |   ESPPreferenceObject pref_; | ||||||
|  |  | ||||||
|  | #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) | ||||||
|  |   std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr; | ||||||
|  |   std::unique_ptr<socket::Socket> listen_socket_ = nullptr; | ||||||
|  |   std::vector<struct sockaddr> sockaddrs_{}; | ||||||
|  | #else | ||||||
|  |   std::vector<IPAddress> ipaddrs_{}; | ||||||
|  |   WiFiUDP udp_client_{}; | ||||||
|  | #endif | ||||||
|  |   std::vector<uint8_t> encryption_key_{}; | ||||||
|  |   std::vector<std::string> addresses_{}; | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   std::vector<Sensor> sensors_{}; | ||||||
|  |   std::map<std::string, std::map<std::string, sensor::Sensor *>> remote_sensors_{}; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |   std::vector<BinarySensor> binary_sensors_{}; | ||||||
|  |   std::map<std::string, std::map<std::string, binary_sensor::BinarySensor *>> remote_binary_sensors_{}; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   std::map<std::string, Provider> providers_{}; | ||||||
|  |   std::vector<uint8_t> ping_header_{}; | ||||||
|  |   std::vector<uint8_t> header_{}; | ||||||
|  |   std::vector<uint8_t> data_{}; | ||||||
|  |   std::map<const char *, uint32_t> 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 | ||||||
							
								
								
									
										35
									
								
								tests/components/udp/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								tests/components/udp/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | wifi: | ||||||
|  |   ssid: MySSID | ||||||
|  |   password: password1 | ||||||
|  |  | ||||||
|  | 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: 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 | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.bk72xx-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.bk72xx-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.esp32-c3-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.esp32-c3-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.esp8266-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										4
									
								
								tests/components/udp/test.host.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/udp/test.host.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | packages: | ||||||
|  |   common: !include common.yaml | ||||||
|  |  | ||||||
|  | wifi: !remove | ||||||
							
								
								
									
										1
									
								
								tests/components/udp/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/udp/test.rp2040-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
		Reference in New Issue
	
	Block a user