diff --git a/esphome/components/openthread_info/__init__.py b/esphome/components/openthread_info/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.cpp b/esphome/components/openthread_info/openthread_info_text_sensor.cpp new file mode 100644 index 0000000000..e455b98429 --- /dev/null +++ b/esphome/components/openthread_info/openthread_info_text_sensor.cpp @@ -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 Role", 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 diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h new file mode 100644 index 0000000000..a881848e34 --- /dev/null +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -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::OpenThreadLockGuard; + +class OpenThreadInstancePollingComponent : public PollingComponent { + public: + void update() override { + auto lock = OpenThreadLockGuard::try_acquire(100); + 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 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_pretty(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 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_pretty(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 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_pretty(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 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_pretty(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 last_extpanid_{}; +}; + +} // namespace openthread_info +} // namespace esphome +#endif diff --git a/esphome/components/openthread_info/text_sensor.py b/esphome/components/openthread_info/text_sensor.py new file mode 100644 index 0000000000..b0b74db754 --- /dev/null +++ b/esphome/components/openthread_info/text_sensor.py @@ -0,0 +1,107 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +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_EXTADDR = "extaddr" + +# TODO: Move these to const.py +CONF_NETWORK_NAME = "network_name" +CONF_NETWORK_KEY = "network_key" +CONF_PSKC = "pskc" +CONF_PANID = "panid" +CONF_EXTPANID = "extpanid" + + +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_EXTADDR): 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_PANID): text_sensor.text_sensor_schema( + PanIdOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ).extend(cv.polling_component_schema("1s")), + cv.Optional(CONF_EXTPANID): text_sensor.text_sensor_schema( + ExtPanIdOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ).extend(cv.polling_component_schema("1s")), + } +) + + +async def setup_conf(config, key): + if key in config: + conf = config[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_EXTADDR) + 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_PANID) + await setup_conf(config, CONF_EXTPANID) diff --git a/tests/components/openthread/test-ot.yaml b/tests/components/openthread/test-ot.yaml index 49eceb16d3..80a4110abb 100644 --- a/tests/components/openthread/test-ot.yaml +++ b/tests/components/openthread/test-ot.yaml @@ -23,9 +23,28 @@ openthread: extpanid: d63e8e3e495ebbc3 pskc: c23a76e98f1a6483639b1ac1271e2e27 -# The web server will cause the HA integration to fail, see note in the README -# web_server: -# port: 80 +text_sensor: + - platform: openthread_info + ip_address: + name: "Off-mesh routable IP Address" + channel: + name: "Channel" + role: + name: "Device Role" + rloc16: + name: "RLOC16" + extaddr: + name: "Extended Address" + eui64: + name: "EUI64" + network_name: + name: "Network Name" + network_key: + name: "Network Key" + panid: + name: "PAN ID" + extpanid: + name: "Extended PAN ID" api: encryption: