mirror of
https://github.com/esphome/esphome.git
synced 2026-05-22 01:42:49 +08: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/uart/* @esphome/core
|
||||
esphome/components/uart/button/* @ssieb
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
|
||||
@@ -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)))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
wifi: !remove
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user