mirror of
https://github.com/esphome/esphome.git
synced 2025-06-15 06:46:59 +02:00
Add OpenThread support on ESP-IDF (#7506)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
487e1f871f
commit
9d9d210176
@ -322,6 +322,7 @@ esphome/components/number/* @esphome/core
|
|||||||
esphome/components/one_wire/* @ssieb
|
esphome/components/one_wire/* @ssieb
|
||||||
esphome/components/online_image/* @clydebarrow @guillempages
|
esphome/components/online_image/* @clydebarrow @guillempages
|
||||||
esphome/components/opentherm/* @olegtarasov
|
esphome/components/opentherm/* @olegtarasov
|
||||||
|
esphome/components/openthread/* @mrene
|
||||||
esphome/components/ota/* @esphome/core
|
esphome/components/ota/* @esphome/core
|
||||||
esphome/components/output/* @esphome/core
|
esphome/components/output/* @esphome/core
|
||||||
esphome/components/packet_transport/* @clydebarrow
|
esphome/components/packet_transport/* @clydebarrow
|
||||||
|
@ -59,6 +59,8 @@ void MDNSComponent::compile_records_() {
|
|||||||
service.txt_records.push_back({"network", "wifi"});
|
service.txt_records.push_back({"network", "wifi"});
|
||||||
#elif defined(USE_ETHERNET)
|
#elif defined(USE_ETHERNET)
|
||||||
service.txt_records.push_back({"network", "ethernet"});
|
service.txt_records.push_back({"network", "ethernet"});
|
||||||
|
#elif defined(USE_OPENTHREAD)
|
||||||
|
service.txt_records.push_back({"network", "thread"});
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
@ -132,6 +134,8 @@ void MDNSComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<MDNSService> MDNSComponent::get_services() { return this->services_; }
|
||||||
|
|
||||||
} // namespace mdns
|
} // namespace mdns
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
#endif
|
#endif
|
||||||
|
@ -37,6 +37,8 @@ class MDNSComponent : public Component {
|
|||||||
|
|
||||||
void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); }
|
void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); }
|
||||||
|
|
||||||
|
std::vector<MDNSService> get_services();
|
||||||
|
|
||||||
void on_shutdown() override;
|
void on_shutdown() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -9,6 +9,10 @@
|
|||||||
#include "esphome/components/ethernet/ethernet_component.h"
|
#include "esphome/components/ethernet/ethernet_component.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
#include "esphome/components/openthread/openthread.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace network {
|
namespace network {
|
||||||
|
|
||||||
@ -23,6 +27,11 @@ bool is_connected() {
|
|||||||
return wifi::global_wifi_component->is_connected();
|
return wifi::global_wifi_component->is_connected();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
if (openthread::global_openthread_component != nullptr)
|
||||||
|
return openthread::global_openthread_component->is_connected();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_HOST
|
#ifdef USE_HOST
|
||||||
return true; // Assume its connected
|
return true; // Assume its connected
|
||||||
#endif
|
#endif
|
||||||
@ -45,6 +54,10 @@ network::IPAddresses get_ip_addresses() {
|
|||||||
#ifdef USE_WIFI
|
#ifdef USE_WIFI
|
||||||
if (wifi::global_wifi_component != nullptr)
|
if (wifi::global_wifi_component != nullptr)
|
||||||
return wifi::global_wifi_component->get_ip_addresses();
|
return wifi::global_wifi_component->get_ip_addresses();
|
||||||
|
#endif
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
if (openthread::global_openthread_component != nullptr)
|
||||||
|
return openthread::global_openthread_component->get_ip_addresses();
|
||||||
#endif
|
#endif
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
148
esphome/components/openthread/__init__.py
Normal file
148
esphome/components/openthread/__init__.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.esp32 import (
|
||||||
|
VARIANT_ESP32C6,
|
||||||
|
VARIANT_ESP32H2,
|
||||||
|
add_idf_sdkconfig_option,
|
||||||
|
only_on_variant,
|
||||||
|
)
|
||||||
|
from esphome.components.mdns import MDNSComponent
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID
|
||||||
|
from esphome.core import CORE
|
||||||
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_EXT_PAN_ID,
|
||||||
|
CONF_FORCE_DATASET,
|
||||||
|
CONF_MDNS_ID,
|
||||||
|
CONF_MESH_LOCAL_PREFIX,
|
||||||
|
CONF_NETWORK_KEY,
|
||||||
|
CONF_NETWORK_NAME,
|
||||||
|
CONF_PAN_ID,
|
||||||
|
CONF_PSKC,
|
||||||
|
CONF_SRP_ID,
|
||||||
|
CONF_TLV,
|
||||||
|
)
|
||||||
|
from .tlv import parse_tlv
|
||||||
|
|
||||||
|
CODEOWNERS = ["@mrene"]
|
||||||
|
|
||||||
|
AUTO_LOAD = ["network"]
|
||||||
|
|
||||||
|
# Wi-fi / Bluetooth / Thread coexistence isn't implemented at this time
|
||||||
|
# TODO: Doesn't conflict with wifi if you're using another ESP as an RCP (radio coprocessor), but this isn't implemented yet
|
||||||
|
CONFLICTS_WITH = ["wifi"]
|
||||||
|
DEPENDENCIES = ["esp32"]
|
||||||
|
|
||||||
|
|
||||||
|
def set_sdkconfig_options(config):
|
||||||
|
# and expose options for using SPI/UART RCPs
|
||||||
|
add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_RADIO_NATIVE", True)
|
||||||
|
|
||||||
|
# There is a conflict if the logger's uart also uses the default UART, which is seen as a watchdog failure on "ot_cli"
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False)
|
||||||
|
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID])
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL])
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if network_name := config.get(CONF_NETWORK_NAME):
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name)
|
||||||
|
|
||||||
|
if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None:
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}"
|
||||||
|
)
|
||||||
|
if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None:
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}"
|
||||||
|
)
|
||||||
|
if (pskc := config.get(CONF_PSKC)) is not None:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}")
|
||||||
|
|
||||||
|
if CONF_FORCE_DATASET in config:
|
||||||
|
if config[CONF_FORCE_DATASET]:
|
||||||
|
cg.add_define("CONFIG_OPENTHREAD_FORCE_DATASET")
|
||||||
|
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
|
||||||
|
|
||||||
|
# TODO: Add suport for sleepy end devices
|
||||||
|
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_FTD", True) # Full Thread Device
|
||||||
|
|
||||||
|
|
||||||
|
openthread_ns = cg.esphome_ns.namespace("openthread")
|
||||||
|
OpenThreadComponent = openthread_ns.class_("OpenThreadComponent", cg.Component)
|
||||||
|
OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Component)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_tlv(config):
|
||||||
|
if tlv := config.get(CONF_TLV):
|
||||||
|
config = config.copy()
|
||||||
|
parsed_tlv = parse_tlv(tlv)
|
||||||
|
validated = _CONNECTION_SCHEMA(parsed_tlv)
|
||||||
|
config.update(validated)
|
||||||
|
del config[CONF_TLV]
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
_CONNECTION_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Inclusive(CONF_PAN_ID, "manual"): cv.hex_int,
|
||||||
|
cv.Inclusive(CONF_CHANNEL, "manual"): cv.int_,
|
||||||
|
cv.Inclusive(CONF_NETWORK_KEY, "manual"): cv.hex_int,
|
||||||
|
cv.Optional(CONF_EXT_PAN_ID): cv.hex_int,
|
||||||
|
cv.Optional(CONF_NETWORK_NAME): cv.string_strict,
|
||||||
|
cv.Optional(CONF_PSKC): cv.hex_int,
|
||||||
|
cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(OpenThreadComponent),
|
||||||
|
cv.GenerateID(CONF_SRP_ID): cv.declare_id(OpenThreadSrpComponent),
|
||||||
|
cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent),
|
||||||
|
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
|
||||||
|
cv.Optional(CONF_TLV): cv.string_strict,
|
||||||
|
}
|
||||||
|
).extend(_CONNECTION_SCHEMA),
|
||||||
|
cv.has_exactly_one_key(CONF_PAN_ID, CONF_TLV),
|
||||||
|
_convert_tlv,
|
||||||
|
cv.only_with_esp_idf,
|
||||||
|
only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(_):
|
||||||
|
full_config = fv.full_config.get()
|
||||||
|
network_config = full_config.get("network", {})
|
||||||
|
if not network_config.get(CONF_ENABLE_IPV6, False):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"OpenThread requires IPv6 to be enabled in the network component. "
|
||||||
|
"Please set `enable_ipv6: true` in the `network` configuration."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
cg.add_define("USE_OPENTHREAD")
|
||||||
|
|
||||||
|
ot = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(ot, config)
|
||||||
|
|
||||||
|
srp = cg.new_Pvariable(config[CONF_SRP_ID])
|
||||||
|
cg.add(srp.set_host_name(cg.RawExpression(f'"{CORE.name}"')))
|
||||||
|
mdns_component = await cg.get_variable(config[CONF_MDNS_ID])
|
||||||
|
cg.add(srp.set_mdns(mdns_component))
|
||||||
|
await cg.register_component(srp, config)
|
||||||
|
|
||||||
|
set_sdkconfig_options(config)
|
10
esphome/components/openthread/const.py
Normal file
10
esphome/components/openthread/const.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CONF_EXT_PAN_ID = "ext_pan_id"
|
||||||
|
CONF_FORCE_DATASET = "force_dataset"
|
||||||
|
CONF_MDNS_ID = "mdns_id"
|
||||||
|
CONF_MESH_LOCAL_PREFIX = "mesh_local_prefix"
|
||||||
|
CONF_NETWORK_NAME = "network_name"
|
||||||
|
CONF_NETWORK_KEY = "network_key"
|
||||||
|
CONF_PAN_ID = "pan_id"
|
||||||
|
CONF_PSKC = "pskc"
|
||||||
|
CONF_SRP_ID = "srp_id"
|
||||||
|
CONF_TLV = "tlv"
|
201
esphome/components/openthread/openthread.cpp
Normal file
201
esphome/components/openthread/openthread.cpp
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
#include "openthread.h"
|
||||||
|
|
||||||
|
#include <freertos/portmacro.h>
|
||||||
|
|
||||||
|
#include <openthread/cli.h>
|
||||||
|
#include <openthread/instance.h>
|
||||||
|
#include <openthread/logging.h>
|
||||||
|
#include <openthread/netdata.h>
|
||||||
|
#include <openthread/srp_client.h>
|
||||||
|
#include <openthread/srp_client_buffers.h>
|
||||||
|
#include <openthread/tasklet.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#define TAG "openthread"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace openthread {
|
||||||
|
|
||||||
|
OpenThreadComponent *global_openthread_component = nullptr;
|
||||||
|
|
||||||
|
OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; }
|
||||||
|
|
||||||
|
OpenThreadComponent::~OpenThreadComponent() {
|
||||||
|
auto lock = InstanceLock::try_acquire(100);
|
||||||
|
if (!lock) {
|
||||||
|
ESP_LOGW(TAG, "Failed to acquire OpenThread lock in destructor, leaking memory");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
otInstance *instance = lock->get_instance();
|
||||||
|
otSrpClientClearHostAndServices(instance);
|
||||||
|
otSrpClientBuffersFreeAllServices(instance);
|
||||||
|
global_openthread_component = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenThreadComponent::is_connected() {
|
||||||
|
auto lock = InstanceLock::try_acquire(100);
|
||||||
|
if (!lock) {
|
||||||
|
ESP_LOGW(TAG, "Failed to acquire OpenThread lock in is_connected");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
otInstance *instance = lock->get_instance();
|
||||||
|
if (instance == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
otDeviceRole role = otThreadGetDeviceRole(instance);
|
||||||
|
|
||||||
|
// TODO: If we're a leader, check that there is at least 1 known peer
|
||||||
|
return role >= OT_DEVICE_ROLE_CHILD;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the off-mesh routable address
|
||||||
|
std::optional<otIp6Address> OpenThreadComponent::get_omr_address() {
|
||||||
|
auto lock = InstanceLock::acquire();
|
||||||
|
return this->get_omr_address_(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<otIp6Address> OpenThreadComponent::get_omr_address_(std::optional<InstanceLock> &lock) {
|
||||||
|
otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT;
|
||||||
|
otInstance *instance = nullptr;
|
||||||
|
|
||||||
|
instance = lock->get_instance();
|
||||||
|
|
||||||
|
otBorderRouterConfig aConfig;
|
||||||
|
if (otNetDataGetNextOnMeshPrefix(instance, &iterator, &aConfig) != OT_ERROR_NONE) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otIp6Prefix *omrPrefix = &aConfig.mPrefix;
|
||||||
|
const otNetifAddress *unicastAddrs = otIp6GetUnicastAddresses(instance);
|
||||||
|
for (const otNetifAddress *addr = unicastAddrs; addr; addr = addr->mNext) {
|
||||||
|
const otIp6Address *localIp = &addr->mAddress;
|
||||||
|
if (otIp6PrefixMatch(&omrPrefix->mPrefix, localIp)) {
|
||||||
|
return *localIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void srpCallback(otError aError, const otSrpClientHostInfo *aHostInfo, const otSrpClientService *aServices,
|
||||||
|
const otSrpClientService *aRemovedServices, void *aContext) {
|
||||||
|
if (aError != 0) {
|
||||||
|
ESP_LOGW(TAG, "SRP client reported an error: %s", otThreadErrorToString(aError));
|
||||||
|
for (const otSrpClientHostInfo *host = aHostInfo; host; host = nullptr) {
|
||||||
|
ESP_LOGW(TAG, " Host: %s", host->mName);
|
||||||
|
}
|
||||||
|
for (const otSrpClientService *service = aServices; service; service = service->mNext) {
|
||||||
|
ESP_LOGW(TAG, " Service: %s", service->mName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void srpStartCallback(const otSockAddr *aServerSockAddr, void *aContext) { ESP_LOGI(TAG, "SRP client has started"); }
|
||||||
|
|
||||||
|
void OpenThreadSrpComponent::setup() {
|
||||||
|
otError error;
|
||||||
|
auto lock = InstanceLock::acquire();
|
||||||
|
otInstance *instance = lock->get_instance();
|
||||||
|
|
||||||
|
otSrpClientSetCallback(instance, srpCallback, nullptr);
|
||||||
|
|
||||||
|
// set the host name
|
||||||
|
uint16_t size;
|
||||||
|
char *existing_host_name = otSrpClientBuffersGetHostNameString(instance, &size);
|
||||||
|
uint16_t len = this->host_name_.size();
|
||||||
|
if (len > size) {
|
||||||
|
ESP_LOGW(TAG, "Hostname is too long, choose a shorter project name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(existing_host_name, this->host_name_.c_str(), len + 1);
|
||||||
|
|
||||||
|
error = otSrpClientSetHostName(instance, existing_host_name);
|
||||||
|
if (error != 0) {
|
||||||
|
ESP_LOGW(TAG, "Could not set host name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = otSrpClientEnableAutoHostAddress(instance);
|
||||||
|
if (error != 0) {
|
||||||
|
ESP_LOGW(TAG, "Could not enable auto host address");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this
|
||||||
|
// component
|
||||||
|
this->mdns_services_ = this->mdns_->get_services();
|
||||||
|
ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size());
|
||||||
|
for (const auto &service : this->mdns_services_) {
|
||||||
|
otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
|
||||||
|
if (!entry) {
|
||||||
|
ESP_LOGW(TAG, "Failed to allocate service entry");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set service name
|
||||||
|
char *string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
|
||||||
|
std::string full_service = service.service_type + "." + service.proto;
|
||||||
|
if (full_service.size() > size) {
|
||||||
|
ESP_LOGW(TAG, "Service name too long: %s", full_service.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
memcpy(string, full_service.c_str(), full_service.size() + 1);
|
||||||
|
|
||||||
|
// Set instance name (using host_name)
|
||||||
|
string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
|
||||||
|
if (this->host_name_.size() > size) {
|
||||||
|
ESP_LOGW(TAG, "Instance name too long: %s", this->host_name_.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
memcpy(string, this->host_name_.c_str(), this->host_name_.size() + 1);
|
||||||
|
|
||||||
|
// Set port
|
||||||
|
entry->mService.mPort = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
|
||||||
|
|
||||||
|
otDnsTxtEntry *mTxtEntries =
|
||||||
|
reinterpret_cast<otDnsTxtEntry *>(this->pool_alloc_(sizeof(otDnsTxtEntry) * service.txt_records.size()));
|
||||||
|
// Set TXT records
|
||||||
|
entry->mService.mNumTxtEntries = service.txt_records.size();
|
||||||
|
for (size_t i = 0; i < service.txt_records.size(); i++) {
|
||||||
|
const auto &txt = service.txt_records[i];
|
||||||
|
auto value = const_cast<TemplatableValue<std::string> &>(txt.value).value();
|
||||||
|
mTxtEntries[i].mKey = txt.key.c_str();
|
||||||
|
mTxtEntries[i].mValue = reinterpret_cast<const uint8_t *>(value.c_str());
|
||||||
|
mTxtEntries[i].mValueLength = value.size();
|
||||||
|
}
|
||||||
|
entry->mService.mTxtEntries = mTxtEntries;
|
||||||
|
entry->mService.mNumTxtEntries = service.txt_records.size();
|
||||||
|
|
||||||
|
// Add service
|
||||||
|
error = otSrpClientAddService(instance, &entry->mService);
|
||||||
|
if (error != OT_ERROR_NONE) {
|
||||||
|
ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error));
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "Added service: %s", full_service.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
otSrpClientEnableAutoStartMode(instance, srpStartCallback, nullptr);
|
||||||
|
ESP_LOGW(TAG, "Finished SRP setup **** ");
|
||||||
|
}
|
||||||
|
|
||||||
|
void *OpenThreadSrpComponent::pool_alloc_(size_t size) {
|
||||||
|
uint8_t *ptr = new uint8_t[size];
|
||||||
|
this->memory_pool_.emplace_back(std::unique_ptr<uint8_t[]>(ptr));
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenThreadSrpComponent::set_host_name(std::string host_name) { this->host_name_ = host_name; }
|
||||||
|
|
||||||
|
void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; }
|
||||||
|
|
||||||
|
} // namespace openthread
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
72
esphome/components/openthread/openthread.h
Normal file
72
esphome/components/openthread/openthread.h
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/mdns/mdns_component.h"
|
||||||
|
#include "esphome/components/network/ip_address.h"
|
||||||
|
|
||||||
|
#include <openthread/thread.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace openthread {
|
||||||
|
|
||||||
|
class InstanceLock;
|
||||||
|
|
||||||
|
class OpenThreadComponent : public Component {
|
||||||
|
public:
|
||||||
|
OpenThreadComponent();
|
||||||
|
~OpenThreadComponent();
|
||||||
|
void setup() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::WIFI; }
|
||||||
|
|
||||||
|
bool is_connected();
|
||||||
|
network::IPAddresses get_ip_addresses();
|
||||||
|
std::optional<otIp6Address> get_omr_address();
|
||||||
|
void ot_main();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::optional<otIp6Address> get_omr_address_(std::optional<InstanceLock> &lock);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern OpenThreadComponent *global_openthread_component;
|
||||||
|
|
||||||
|
class OpenThreadSrpComponent : public Component {
|
||||||
|
public:
|
||||||
|
void set_mdns(esphome::mdns::MDNSComponent *mdns);
|
||||||
|
void set_host_name(std::string host_name);
|
||||||
|
// This has to run after the mdns component or else no services are available to advertise
|
||||||
|
float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string host_name_;
|
||||||
|
esphome::mdns::MDNSComponent *mdns_{nullptr};
|
||||||
|
std::vector<esphome::mdns::MDNSService> mdns_services_;
|
||||||
|
std::vector<std::unique_ptr<uint8_t[]>> memory_pool_;
|
||||||
|
void *pool_alloc_(size_t size);
|
||||||
|
};
|
||||||
|
|
||||||
|
class InstanceLock {
|
||||||
|
public:
|
||||||
|
static std::optional<InstanceLock> try_acquire(int delay);
|
||||||
|
static std::optional<InstanceLock> acquire();
|
||||||
|
~InstanceLock();
|
||||||
|
|
||||||
|
// Returns the global openthread instance guarded by this lock
|
||||||
|
otInstance *get_instance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Use a private constructor in order to force thehandling
|
||||||
|
// of acquisition failure
|
||||||
|
InstanceLock() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace openthread
|
||||||
|
} // namespace esphome
|
||||||
|
#endif
|
166
esphome/components/openthread/openthread_esp.cpp
Normal file
166
esphome/components/openthread/openthread_esp.cpp
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_OPENTHREAD) && defined(USE_ESP_IDF)
|
||||||
|
#include "openthread.h"
|
||||||
|
#include <openthread/logging.h>
|
||||||
|
|
||||||
|
#include "esp_openthread.h"
|
||||||
|
#include "esp_openthread_lock.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
|
|
||||||
|
#include "esp_openthread_cli.h"
|
||||||
|
#include "esp_openthread_netif_glue.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
#include "esp_vfs_eventfd.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_netif_types.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
#define TAG "openthread"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace openthread {
|
||||||
|
|
||||||
|
void OpenThreadComponent::setup() {
|
||||||
|
ESP_LOGI(TAG, "Setting up OpenThread...");
|
||||||
|
// Used eventfds:
|
||||||
|
// * netif
|
||||||
|
// * ot task queue
|
||||||
|
// * radio driver
|
||||||
|
esp_vfs_eventfd_config_t eventfd_config = {
|
||||||
|
.max_fds = 3,
|
||||||
|
};
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
|
||||||
|
|
||||||
|
xTaskCreate(
|
||||||
|
[](void *arg) {
|
||||||
|
static_cast<OpenThreadComponent *>(arg)->ot_main();
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
},
|
||||||
|
"ot_main", 10240, this, 5, nullptr);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OpenThread started");
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_netif_t *init_openthread_netif(const esp_openthread_platform_config_t *config) {
|
||||||
|
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_OPENTHREAD();
|
||||||
|
esp_netif_t *netif = esp_netif_new(&cfg);
|
||||||
|
assert(netif != nullptr);
|
||||||
|
ESP_ERROR_CHECK(esp_netif_attach(netif, esp_openthread_netif_glue_init(config)));
|
||||||
|
|
||||||
|
return netif;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenThreadComponent::ot_main() {
|
||||||
|
esp_openthread_platform_config_t config = {
|
||||||
|
.radio_config =
|
||||||
|
{
|
||||||
|
.radio_mode = RADIO_MODE_NATIVE,
|
||||||
|
.radio_uart_config = {},
|
||||||
|
},
|
||||||
|
.host_config =
|
||||||
|
{
|
||||||
|
// There is a conflict between esphome's logger which also
|
||||||
|
// claims the usb serial jtag device.
|
||||||
|
// .host_connection_mode = HOST_CONNECTION_MODE_CLI_USB,
|
||||||
|
// .host_usb_config = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT(),
|
||||||
|
},
|
||||||
|
.port_config =
|
||||||
|
{
|
||||||
|
.storage_partition_name = "nvs",
|
||||||
|
.netif_queue_size = 10,
|
||||||
|
.task_queue_size = 10,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the OpenThread stack
|
||||||
|
// otLoggingSetLevel(OT_LOG_LEVEL_DEBG);
|
||||||
|
ESP_ERROR_CHECK(esp_openthread_init(&config));
|
||||||
|
|
||||||
|
#if CONFIG_OPENTHREAD_STATE_INDICATOR_ENABLE
|
||||||
|
ESP_ERROR_CHECK(esp_openthread_state_indicator_init(esp_openthread_get_instance()));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC
|
||||||
|
// The OpenThread log level directly matches ESP log level
|
||||||
|
(void) otLoggingSetLevel(CONFIG_LOG_DEFAULT_LEVEL);
|
||||||
|
#endif
|
||||||
|
// Initialize the OpenThread cli
|
||||||
|
#if CONFIG_OPENTHREAD_CLI
|
||||||
|
esp_openthread_cli_init();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
esp_netif_t *openthread_netif;
|
||||||
|
// Initialize the esp_netif bindings
|
||||||
|
openthread_netif = init_openthread_netif(&config);
|
||||||
|
esp_netif_set_default_netif(openthread_netif);
|
||||||
|
|
||||||
|
#if CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||||
|
esp_cli_custom_command_init();
|
||||||
|
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||||
|
|
||||||
|
// Run the main loop
|
||||||
|
#if CONFIG_OPENTHREAD_CLI
|
||||||
|
esp_openthread_cli_create_task();
|
||||||
|
#endif
|
||||||
|
ESP_LOGI(TAG, "Activating dataset...");
|
||||||
|
otOperationalDatasetTlvs dataset;
|
||||||
|
|
||||||
|
#ifdef CONFIG_OPENTHREAD_FORCE_DATASET
|
||||||
|
ESP_ERROR_CHECK(esp_openthread_auto_start(NULL));
|
||||||
|
#else
|
||||||
|
otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset);
|
||||||
|
ESP_ERROR_CHECK(esp_openthread_auto_start((error == OT_ERROR_NONE) ? &dataset : NULL));
|
||||||
|
#endif
|
||||||
|
esp_openthread_launch_mainloop();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
esp_openthread_netif_glue_deinit();
|
||||||
|
esp_netif_destroy(openthread_netif);
|
||||||
|
|
||||||
|
esp_vfs_eventfd_unregister();
|
||||||
|
}
|
||||||
|
|
||||||
|
network::IPAddresses OpenThreadComponent::get_ip_addresses() {
|
||||||
|
network::IPAddresses addresses;
|
||||||
|
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||||
|
uint8_t count = 0;
|
||||||
|
esp_netif_t *netif = esp_netif_get_default_netif();
|
||||||
|
count = esp_netif_get_all_ip6(netif, if_ip6s);
|
||||||
|
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
|
||||||
|
if (esp_openthread_lock_acquire(delay)) {
|
||||||
|
return InstanceLock();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<InstanceLock> InstanceLock::acquire() {
|
||||||
|
while (!esp_openthread_lock_acquire(100)) {
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
return InstanceLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
otInstance *InstanceLock::get_instance() { return esp_openthread_get_instance(); }
|
||||||
|
|
||||||
|
InstanceLock::~InstanceLock() { esp_openthread_lock_release(); }
|
||||||
|
|
||||||
|
} // namespace openthread
|
||||||
|
} // namespace esphome
|
||||||
|
#endif
|
58
esphome/components/openthread/tlv.py
Normal file
58
esphome/components/openthread/tlv.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
from esphome.const import CONF_CHANNEL
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
CONF_EXT_PAN_ID,
|
||||||
|
CONF_MESH_LOCAL_PREFIX,
|
||||||
|
CONF_NETWORK_KEY,
|
||||||
|
CONF_NETWORK_NAME,
|
||||||
|
CONF_PAN_ID,
|
||||||
|
CONF_PSKC,
|
||||||
|
)
|
||||||
|
|
||||||
|
TLV_TYPES = {
|
||||||
|
0: CONF_CHANNEL,
|
||||||
|
1: CONF_PAN_ID,
|
||||||
|
2: CONF_EXT_PAN_ID,
|
||||||
|
3: CONF_NETWORK_NAME,
|
||||||
|
4: CONF_PSKC,
|
||||||
|
5: CONF_NETWORK_KEY,
|
||||||
|
7: CONF_MESH_LOCAL_PREFIX,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tlv(tlv) -> dict:
|
||||||
|
data = binascii.a2b_hex(tlv)
|
||||||
|
output = {}
|
||||||
|
pos = 0
|
||||||
|
while pos < len(data):
|
||||||
|
tag = data[pos]
|
||||||
|
pos += 1
|
||||||
|
_len = data[pos]
|
||||||
|
pos += 1
|
||||||
|
val = data[pos : pos + _len]
|
||||||
|
pos += _len
|
||||||
|
if tag in TLV_TYPES:
|
||||||
|
if tag == 3:
|
||||||
|
output[TLV_TYPES[tag]] = val.decode("utf-8")
|
||||||
|
else:
|
||||||
|
output[TLV_TYPES[tag]] = int.from_bytes(val)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import sys
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
parsed = parse_tlv(args[0])
|
||||||
|
# print the parsed TLV data
|
||||||
|
for key, value in parsed.items():
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.hex()
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
0
esphome/components/openthread_info/__init__.py
Normal file
0
esphome/components/openthread_info/__init__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
#include "openthread_info_text_sensor.h"
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace openthread_info {
|
||||||
|
|
||||||
|
static const char *const TAG = "openthread_info";
|
||||||
|
|
||||||
|
void IPAddressOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo IPAddress", this); }
|
||||||
|
void RoleOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Role", this); }
|
||||||
|
void ChannelOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Channel", this); }
|
||||||
|
void Rloc16OpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Rloc16", this); }
|
||||||
|
void ExtAddrOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo ExtAddr", this); }
|
||||||
|
void Eui64OpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Eui64", this); }
|
||||||
|
void NetworkNameOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Network Name", this); }
|
||||||
|
void NetworkKeyOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Network Key", this); }
|
||||||
|
void PanIdOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo PAN ID", this); }
|
||||||
|
void ExtPanIdOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "OpenThreadInfo Extended PAN ID", this); }
|
||||||
|
|
||||||
|
} // namespace openthread_info
|
||||||
|
} // namespace esphome
|
||||||
|
#endif
|
228
esphome/components/openthread_info/openthread_info_text_sensor.h
Normal file
228
esphome/components/openthread_info/openthread_info_text_sensor.h
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
|
#include "esphome/components/openthread/openthread.h"
|
||||||
|
#ifdef USE_OPENTHREAD
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace openthread_info {
|
||||||
|
|
||||||
|
using esphome::openthread::InstanceLock;
|
||||||
|
|
||||||
|
class OpenThreadInstancePollingComponent : public PollingComponent {
|
||||||
|
public:
|
||||||
|
void update() override {
|
||||||
|
auto lock = InstanceLock::try_acquire(10);
|
||||||
|
if (!lock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_instance_(lock->get_instance());
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void update_instance_(otInstance *instance) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update() override {
|
||||||
|
std::optional<otIp6Address> address = openthread::global_openthread_component->get_omr_address();
|
||||||
|
if (!address) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char addressAsString[40];
|
||||||
|
otIp6AddressToString(&*address, addressAsString, 40);
|
||||||
|
std::string ip = addressAsString;
|
||||||
|
|
||||||
|
if (this->last_ip_ != ip) {
|
||||||
|
this->last_ip_ = ip;
|
||||||
|
this->publish_state(this->last_ip_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-ip"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string last_ip_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RoleOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_instance_(otInstance *instance) override {
|
||||||
|
otDeviceRole role = otThreadGetDeviceRole(instance);
|
||||||
|
|
||||||
|
if (this->last_role_ != role) {
|
||||||
|
this->last_role_ = role;
|
||||||
|
this->publish_state(otThreadDeviceRoleToString(this->last_role_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-role"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
otDeviceRole last_role_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Rloc16OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_instance_(otInstance *instance) override {
|
||||||
|
uint16_t rloc16 = otThreadGetRloc16(instance);
|
||||||
|
if (this->last_rloc16_ != rloc16) {
|
||||||
|
this->last_rloc16_ = rloc16;
|
||||||
|
char buf[5];
|
||||||
|
snprintf(buf, sizeof(buf), "%04x", rloc16);
|
||||||
|
this->publish_state(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-rloc16"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint16_t last_rloc16_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_instance_(otInstance *instance) override {
|
||||||
|
auto extaddr = otLinkGetExtendedAddress(instance);
|
||||||
|
if (!std::equal(this->last_extaddr_.begin(), this->last_extaddr_.end(), extaddr->m8)) {
|
||||||
|
std::copy(extaddr->m8, extaddr->m8 + 8, this->last_extaddr_.begin());
|
||||||
|
this->publish_state(format_hex(extaddr->m8, 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-extaddr"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::array<uint8_t, 8> last_extaddr_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_instance_(otInstance *instance) override {
|
||||||
|
otExtAddress addr;
|
||||||
|
otLinkGetFactoryAssignedIeeeEui64(instance, &addr);
|
||||||
|
|
||||||
|
if (!std::equal(this->last_eui64_.begin(), this->last_eui64_.end(), addr.m8)) {
|
||||||
|
std::copy(addr.m8, addr.m8 + 8, this->last_eui64_.begin());
|
||||||
|
this->publish_state(format_hex(this->last_eui64_.begin(), 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-extaddr"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::array<uint8_t, 8> last_eui64_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_instance_(otInstance *instance) override {
|
||||||
|
uint8_t channel = otLinkGetChannel(instance);
|
||||||
|
if (this->last_channel_ != channel) {
|
||||||
|
this->last_channel_ = channel;
|
||||||
|
this->publish_state(std::to_string(this->last_channel_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-extaddr"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint8_t last_channel_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DatasetOpenThreadInfo : public OpenThreadInstancePollingComponent {
|
||||||
|
public:
|
||||||
|
void update_instance_(otInstance *instance) override {
|
||||||
|
otOperationalDataset dataset;
|
||||||
|
if (otDatasetGetActive(instance, &dataset) != OT_ERROR_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->update_dataset_(&dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void update_dataset_(otOperationalDataset *dataset) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NetworkNameOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_dataset_(otOperationalDataset *dataset) override {
|
||||||
|
if (this->last_network_name_ != dataset->mNetworkName.m8) {
|
||||||
|
this->last_network_name_ = dataset->mNetworkName.m8;
|
||||||
|
this->publish_state(this->last_network_name_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-networkname"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string last_network_name_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_dataset_(otOperationalDataset *dataset) override {
|
||||||
|
if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) {
|
||||||
|
std::copy(dataset->mNetworkKey.m8, dataset->mNetworkKey.m8 + 16, this->last_key_.begin());
|
||||||
|
this->publish_state(format_hex(dataset->mNetworkKey.m8, 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-networkkey"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::array<uint8_t, 16> last_key_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class PanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_dataset_(otOperationalDataset *dataset) override {
|
||||||
|
uint16_t panid = dataset->mPanId;
|
||||||
|
if (this->last_panid_ != panid) {
|
||||||
|
this->last_panid_ = panid;
|
||||||
|
char buf[5];
|
||||||
|
snprintf(buf, sizeof(buf), "%04x", panid);
|
||||||
|
this->publish_state(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-panid"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint16_t last_panid_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update_dataset_(otOperationalDataset *dataset) override {
|
||||||
|
if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) {
|
||||||
|
std::copy(dataset->mExtendedPanId.m8, dataset->mExtendedPanId.m8 + 8, this->last_extpanid_.begin());
|
||||||
|
this->publish_state(format_hex(this->last_extpanid_.begin(), 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-openthreadinfo-extpanid"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::array<uint8_t, 8> last_extpanid_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace openthread_info
|
||||||
|
} // namespace esphome
|
||||||
|
#endif
|
105
esphome/components/openthread_info/text_sensor.py
Normal file
105
esphome/components/openthread_info/text_sensor.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import text_sensor
|
||||||
|
from esphome.components.openthread.const import (
|
||||||
|
CONF_EXT_PAN_ID,
|
||||||
|
CONF_NETWORK_KEY,
|
||||||
|
CONF_NETWORK_NAME,
|
||||||
|
CONF_PAN_ID,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_CHANNEL, CONF_IP_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
|
||||||
|
CONF_ROLE = "role"
|
||||||
|
CONF_RLOC16 = "rloc16"
|
||||||
|
CONF_EUI64 = "eui64"
|
||||||
|
CONF_EXT_ADDR = "ext_addr"
|
||||||
|
|
||||||
|
|
||||||
|
DEPENDENCIES = ["openthread"]
|
||||||
|
|
||||||
|
openthread_info_ns = cg.esphome_ns.namespace("openthread_info")
|
||||||
|
IPAddressOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"IPAddressOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
RoleOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"RoleOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
Rloc16OpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"Rloc16OpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
ExtAddrOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"ExtAddrOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
Eui64OpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"Eui64OpenThreadInfo", text_sensor.TextSensor, cg.Component
|
||||||
|
)
|
||||||
|
ChannelOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"ChannelOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
NetworkNameOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"NetworkNameOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
NetworkKeyOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"NetworkKeyOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
PanIdOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"PanIdOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
ExtPanIdOpenThreadInfo = openthread_info_ns.class_(
|
||||||
|
"ExtPanIdOpenThreadInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema(
|
||||||
|
IPAddressOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_ROLE): text_sensor.text_sensor_schema(
|
||||||
|
RoleOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_RLOC16): text_sensor.text_sensor_schema(
|
||||||
|
Rloc16OpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_EXT_ADDR): text_sensor.text_sensor_schema(
|
||||||
|
ExtAddrOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_EUI64): text_sensor.text_sensor_schema(
|
||||||
|
Eui64OpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1h")),
|
||||||
|
cv.Optional(CONF_CHANNEL): text_sensor.text_sensor_schema(
|
||||||
|
ChannelOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_NETWORK_NAME): text_sensor.text_sensor_schema(
|
||||||
|
NetworkNameOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_NETWORK_KEY): text_sensor.text_sensor_schema(
|
||||||
|
NetworkKeyOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_PAN_ID): text_sensor.text_sensor_schema( # noqa: F821
|
||||||
|
PanIdOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
cv.Optional(CONF_EXT_PAN_ID): text_sensor.text_sensor_schema(
|
||||||
|
ExtPanIdOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_conf(config: dict, key: str):
|
||||||
|
if conf := config.get(key):
|
||||||
|
var = await text_sensor.new_text_sensor(conf)
|
||||||
|
await cg.register_component(var, conf)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
await setup_conf(config, CONF_IP_ADDRESS)
|
||||||
|
await setup_conf(config, CONF_ROLE)
|
||||||
|
await setup_conf(config, CONF_RLOC16)
|
||||||
|
await setup_conf(config, CONF_EXT_ADDR)
|
||||||
|
await setup_conf(config, CONF_EUI64)
|
||||||
|
await setup_conf(config, CONF_CHANNEL)
|
||||||
|
await setup_conf(config, CONF_NETWORK_NAME)
|
||||||
|
await setup_conf(config, CONF_NETWORK_KEY)
|
||||||
|
await setup_conf(config, CONF_PAN_ID)
|
||||||
|
await setup_conf(config, CONF_EXT_PAN_ID)
|
@ -157,6 +157,9 @@
|
|||||||
#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(5, 3, 2)
|
#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(5, 3, 2)
|
||||||
#define USE_MICRO_WAKE_WORD
|
#define USE_MICRO_WAKE_WORD
|
||||||
#define USE_MICRO_WAKE_WORD_VAD
|
#define USE_MICRO_WAKE_WORD_VAD
|
||||||
|
#if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
|
||||||
|
#define USE_OPENTHREAD
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP32_VARIANT_ESP32S2)
|
#if defined(USE_ESP32_VARIANT_ESP32S2)
|
||||||
|
11
tests/components/openthread/test.esp32-c6-idf.yaml
Normal file
11
tests/components/openthread/test.esp32-c6-idf.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
openthread:
|
||||||
|
channel: 13
|
||||||
|
network_name: OpenThread-8f28
|
||||||
|
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
|
||||||
|
pan_id: 0x8f28
|
||||||
|
ext_pan_id: 0xd63e8e3e495ebbc3
|
||||||
|
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
|
||||||
|
force_dataset: true
|
30
tests/components/openthread_info/test.esp32-c6-idf.yaml
Normal file
30
tests/components/openthread_info/test.esp32-c6-idf.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
openthread:
|
||||||
|
channel: 13
|
||||||
|
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
|
||||||
|
pan_id: 0x8f28
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: openthread_info
|
||||||
|
ip_address:
|
||||||
|
name: "Off-mesh routable IP Address"
|
||||||
|
channel:
|
||||||
|
name: "Channel"
|
||||||
|
role:
|
||||||
|
name: "Device Role"
|
||||||
|
rloc16:
|
||||||
|
name: "RLOC16"
|
||||||
|
ext_addr:
|
||||||
|
name: "Extended Address"
|
||||||
|
eui64:
|
||||||
|
name: "EUI64"
|
||||||
|
network_name:
|
||||||
|
name: "Network Name"
|
||||||
|
network_key:
|
||||||
|
name: "Network Key"
|
||||||
|
pan_id:
|
||||||
|
name: "PAN ID"
|
||||||
|
ext_pan_id:
|
||||||
|
name: "Extended PAN ID"
|
Loading…
x
Reference in New Issue
Block a user