[openthread_info] Add more sensors for Openthread info (#17276)

This commit is contained in:
Ardumine
2026-07-31 12:13:44 -04:00
committed by GitHub
parent 7ad662781d
commit 15711f8112
4 changed files with 372 additions and 0 deletions
@@ -0,0 +1,24 @@
#include "openthread_info_sensor.h"
#if defined(USE_OPENTHREAD) && defined(USE_SENSOR)
#include "esphome/core/log.h"
namespace esphome::openthread_info {
static const char *const TAG = "openthread_info";
void ParentAverageRssiOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Average RSSI", this); }
void ParentLastRssiOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Last RSSI", this); }
void ParentLinkQualityInOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Link Quality In", this); }
void ParentLinkQualityOutOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Link Quality Out", this); }
void TxTotalOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX Total", this); }
void TxRetriesOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX Retries", this); }
void TxErrCcaOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX CCA Errors", this); }
void TxErrAbortOpenThreadInfo::dump_config() { LOG_SENSOR("", "TX Abort Errors", this); }
void RxTotalOpenThreadInfo::dump_config() { LOG_SENSOR("", "RX Total", this); }
void RxErrFcsOpenThreadInfo::dump_config() { LOG_SENSOR("", "RX FCS Errors", this); }
void AttachAttemptsOpenThreadInfo::dump_config() { LOG_SENSOR("", "Attach Attempts", this); }
void ParentChangesOpenThreadInfo::dump_config() { LOG_SENSOR("", "Parent Changes", this); }
void PartitionIdChangesOpenThreadInfo::dump_config() { LOG_SENSOR("", "Partition ID Changes", this); }
} // namespace esphome::openthread_info
#endif
@@ -0,0 +1,131 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_OPENTHREAD) && defined(USE_SENSOR)
#include "openthread_info_text_sensor.h"
#include "esphome/components/sensor/sensor.h"
#include <openthread/link.h>
#include <openthread/thread.h>
namespace esphome::openthread_info {
// Parent RSSI (average) in dBm. Only valid when device is a child; skips publish otherwise.
class ParentAverageRssiOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
int8_t rssi;
if (otThreadGetParentAverageRssi(instance, &rssi) != OT_ERROR_NONE) {
return;
}
this->publish_state(rssi);
}
void dump_config() override;
};
// Parent RSSI (last received frame) in dBm. Only valid when device is a child.
class ParentLastRssiOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
int8_t rssi;
if (otThreadGetParentLastRssi(instance, &rssi) != OT_ERROR_NONE) {
return;
}
this->publish_state(rssi);
}
void dump_config() override;
};
// Incoming link quality from parent (0-3). Only valid when device is a child.
class ParentLinkQualityInOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
otRouterInfo parent_info;
if (otThreadGetParentInfo(instance, &parent_info) != OT_ERROR_NONE) {
return;
}
this->publish_state(parent_info.mLinkQualityIn);
}
void dump_config() override;
};
// Outgoing link quality to parent (0-3). Only valid when device is a child.
class ParentLinkQualityOutOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
otRouterInfo parent_info;
if (otThreadGetParentInfo(instance, &parent_info) != OT_ERROR_NONE) {
return;
}
this->publish_state(parent_info.mLinkQualityOut);
}
void dump_config() override;
};
// --- MAC counters (otLinkGetCounters) — cumulative since boot ---
class TxTotalOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxTotal); }
void dump_config() override;
};
class TxRetriesOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxRetry); }
void dump_config() override;
};
class TxErrCcaOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxErrCca); }
void dump_config() override;
};
class TxErrAbortOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mTxErrAbort); }
void dump_config() override;
};
class RxTotalOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mRxTotal); }
void dump_config() override;
};
class RxErrFcsOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override { this->publish_state(otLinkGetCounters(instance)->mRxErrFcs); }
void dump_config() override;
};
// --- MLE stability counters (otThreadGetMleCounters) — cumulative since boot ---
class AttachAttemptsOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
this->publish_state(otThreadGetMleCounters(instance)->mAttachAttempts);
}
void dump_config() override;
};
class ParentChangesOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
this->publish_state(otThreadGetMleCounters(instance)->mParentChanges);
}
void dump_config() override;
};
class PartitionIdChangesOpenThreadInfo final : public OpenThreadInstancePollingComponent, public sensor::Sensor {
public:
void update_instance(otInstance *instance) override {
this->publish_state(otThreadGetMleCounters(instance)->mPartitionIdChanges);
}
void dump_config() override;
};
} // namespace esphome::openthread_info
#endif
@@ -0,0 +1,188 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_SIGNAL_STRENGTH,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_DECIBEL_MILLIWATT,
UNIT_EMPTY,
)
CONF_PARENT_AVERAGE_RSSI = "parent_average_rssi"
CONF_PARENT_LAST_RSSI = "parent_last_rssi"
CONF_PARENT_LINK_QUALITY_IN = "parent_link_quality_in"
CONF_PARENT_LINK_QUALITY_OUT = "parent_link_quality_out"
CONF_TX_TOTAL = "tx_total"
CONF_TX_RETRIES = "tx_retries"
CONF_TX_ERR_CCA = "tx_err_cca"
CONF_TX_ERR_ABORT = "tx_err_abort"
CONF_RX_TOTAL = "rx_total"
CONF_RX_ERR_FCS = "rx_err_fcs"
CONF_ATTACH_ATTEMPTS = "attach_attempts"
CONF_PARENT_CHANGES = "parent_changes"
CONF_PARTITION_ID_CHANGES = "partition_id_changes"
DEPENDENCIES = ["openthread"]
openthread_info_ns = cg.esphome_ns.namespace("openthread_info")
ParentAverageRssiOpenThreadInfo = openthread_info_ns.class_(
"ParentAverageRssiOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
ParentLastRssiOpenThreadInfo = openthread_info_ns.class_(
"ParentLastRssiOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
ParentLinkQualityInOpenThreadInfo = openthread_info_ns.class_(
"ParentLinkQualityInOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
ParentLinkQualityOutOpenThreadInfo = openthread_info_ns.class_(
"ParentLinkQualityOutOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
TxTotalOpenThreadInfo = openthread_info_ns.class_(
"TxTotalOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
TxRetriesOpenThreadInfo = openthread_info_ns.class_(
"TxRetriesOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
TxErrCcaOpenThreadInfo = openthread_info_ns.class_(
"TxErrCcaOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
TxErrAbortOpenThreadInfo = openthread_info_ns.class_(
"TxErrAbortOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
RxTotalOpenThreadInfo = openthread_info_ns.class_(
"RxTotalOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
RxErrFcsOpenThreadInfo = openthread_info_ns.class_(
"RxErrFcsOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
AttachAttemptsOpenThreadInfo = openthread_info_ns.class_(
"AttachAttemptsOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
ParentChangesOpenThreadInfo = openthread_info_ns.class_(
"ParentChangesOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
PartitionIdChangesOpenThreadInfo = openthread_info_ns.class_(
"PartitionIdChangesOpenThreadInfo", sensor.Sensor, cg.PollingComponent
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PARENT_AVERAGE_RSSI): sensor.sensor_schema(
ParentAverageRssiOpenThreadInfo,
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("5s")),
cv.Optional(CONF_PARENT_LAST_RSSI): sensor.sensor_schema(
ParentLastRssiOpenThreadInfo,
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("5s")),
cv.Optional(CONF_PARENT_LINK_QUALITY_IN): sensor.sensor_schema(
ParentLinkQualityInOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("5s")),
cv.Optional(CONF_PARENT_LINK_QUALITY_OUT): sensor.sensor_schema(
ParentLinkQualityOutOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("5s")),
cv.Optional(CONF_TX_TOTAL): sensor.sensor_schema(
TxTotalOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_TX_RETRIES): sensor.sensor_schema(
TxRetriesOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_TX_ERR_CCA): sensor.sensor_schema(
TxErrCcaOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_TX_ERR_ABORT): sensor.sensor_schema(
TxErrAbortOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_RX_TOTAL): sensor.sensor_schema(
RxTotalOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_RX_ERR_FCS): sensor.sensor_schema(
RxErrFcsOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_ATTACH_ATTEMPTS): sensor.sensor_schema(
AttachAttemptsOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_PARENT_CHANGES): sensor.sensor_schema(
ParentChangesOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
cv.Optional(CONF_PARTITION_ID_CHANGES): sensor.sensor_schema(
PartitionIdChangesOpenThreadInfo,
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("30s")),
}
)
async def setup_conf(config: dict, key: str):
if conf := config.get(key):
var = await sensor.new_sensor(conf)
await cg.register_component(var, conf)
async def to_code(config):
await setup_conf(config, CONF_PARENT_AVERAGE_RSSI)
await setup_conf(config, CONF_PARENT_LAST_RSSI)
await setup_conf(config, CONF_PARENT_LINK_QUALITY_IN)
await setup_conf(config, CONF_PARENT_LINK_QUALITY_OUT)
await setup_conf(config, CONF_TX_TOTAL)
await setup_conf(config, CONF_TX_RETRIES)
await setup_conf(config, CONF_TX_ERR_CCA)
await setup_conf(config, CONF_TX_ERR_ABORT)
await setup_conf(config, CONF_RX_TOTAL)
await setup_conf(config, CONF_RX_ERR_FCS)
await setup_conf(config, CONF_ATTACH_ATTEMPTS)
await setup_conf(config, CONF_PARENT_CHANGES)
await setup_conf(config, CONF_PARTITION_ID_CHANGES)
@@ -28,3 +28,32 @@ text_sensor:
name: "PAN ID"
ext_pan_id:
name: "Extended PAN ID"
sensor:
- platform: openthread_info
parent_average_rssi:
name: "Parent Average RSSI"
parent_last_rssi:
name: "Parent Last RSSI"
parent_link_quality_in:
name: "Parent Link Quality In"
parent_link_quality_out:
name: "Parent Link Quality Out"
tx_total:
name: "TX Total"
tx_retries:
name: "TX Retries"
tx_err_cca:
name: "TX CCA Errors"
tx_err_abort:
name: "TX Abort Errors"
rx_total:
name: "RX Total"
rx_err_fcs:
name: "RX FCS Errors"
attach_attempts:
name: "Attach Attempts"
parent_changes:
name: "Parent Changes"
partition_id_changes:
name: "Partition ID Changes"